Kick off the server refactor with home and auth

This commit is contained in:
Tim Van Baak 2020-04-24 11:37:55 -07:00
parent 07e62b9665
commit 3077b02508
10 changed files with 374 additions and 341 deletions

View File

@ -69,14 +69,17 @@ def command_run(args):
The default Flask server is not secure, and should The default Flask server is not secure, and should
only be used for development. only be used for development.
""" """
from amanuensis.server import app from amanuensis.server import get_app
from amanuensis.config import get, logger
if get("secret_key") is None: root: RootConfigDirectoryContext = args.root
logger.error("Can't run server without a secret_key. Run generate-sec"
"ret first") with root.read_config() as cfg:
return -1 if cfg.secret_key is None:
app.run(host=args.address, port=args.port, debug=args.debug) logger.error("Can't run server without a secret_key. "
"Run generate-secet first.")
return -1
get_app(root).run(host=args.address, port=args.port, debug=args.debug)
return 0 return 0

View File

@ -4,7 +4,7 @@
"address": "127.0.0.1", "address": "127.0.0.1",
"port": "5000", "port": "5000",
"lexicon_data": "./lexicon", "lexicon_data": "./lexicon",
"static_root": "./static", "static_root": "../resources",
"email": { "email": {
"server": null, "server": null,
"port": null, "port": null,

View File

@ -1,41 +1,38 @@
import os from flask import Flask
from flask import Flask, render_template from amanuensis.config import RootConfigDirectoryContext
from flask_login import LoginManager
from amanuensis.config import get, root
# from amanuensis.server.auth import get_bp as get_auth_bp
from amanuensis.server.home import get_bp as get_home_bp
from amanuensis.server.helpers import register_custom_filters
from amanuensis.server.lexicon import get_bp as get_lex_bp
from amanuensis.user import AnonymousUserModel
from amanuensis.models import ModelFactory from amanuensis.models import ModelFactory
from amanuensis.server.auth import get_login_manager, bp_auth
from amanuensis.server.helpers import register_custom_filters
from amanuensis.server.home import bp_home
# from amanuensis.server.lexicon import bp_lexicon
# Flask app init
static_root = os.path.abspath(get("static_root"))
app = Flask(
__name__,
template_folder="../templates",
static_folder=static_root)
app.secret_key = bytes.fromhex(get('secret_key'))
app.config['model_factory'] = ModelFactory(root)
app.jinja_options['trim_blocks'] = True
app.jinja_options['lstrip_blocks'] = True
register_custom_filters(app)
# Flask-Login init def get_app(root: RootConfigDirectoryContext) -> Flask:
login = LoginManager(app) # Flask app init
login.login_view = 'auth.login' with root.read_config() as cfg:
login.anonymous_user = AnonymousUserModel app = Flask(
__name__,
template_folder='../templates',
static_folder=cfg.static_root
)
app.secret_key = bytes.fromhex(cfg.secret_key)
app.config['root'] = root
app.config['model_factory'] = ModelFactory(root)
app.jinja_options['trim_blocks'] = True
app.jinja_options['lstrip_blocks'] = True
register_custom_filters(app)
# Blueprint inits # Flask-Login init
from amanuensis.server.auth import bp as auth_bp login_manager = get_login_manager(root)
from amanuensis.server.auth import login_manager as login_manager login_manager.init_app(app)
login_manager.init_app(app)
app.register_blueprint(auth_bp)
home_bp = get_home_bp() # Blueprint inits
app.register_blueprint(home_bp) app.register_blueprint(bp_auth)
app.register_blueprint(bp_home)
# app.register_blueprint(bp_lexicon)
lex_bp = get_lex_bp() # import code
app.register_blueprint(lex_bp) # code.interact(local=locals())
return app

View File

@ -1,42 +1,64 @@
import logging
import time import time
from flask import Blueprint, render_template, redirect, url_for, flash, current_app from flask import (
from flask_login import login_user, logout_user, login_required, LoginManager Blueprint,
render_template,
redirect,
url_for,
flash,
current_app)
from flask_login import (
login_user,
logout_user,
login_required,
LoginManager)
from amanuensis.config import logger, json_rw from amanuensis.config import RootConfigDirectoryContext
from amanuensis.server.forms import LoginForm from amanuensis.server.forms import LoginForm
from amanuensis.user import UserModel, AnonymousUserModel from amanuensis.models import ModelFactory, AnonymousUserModel
# TODO refactor login init into a func that takes a root cdc logger = logging.getLogger(__name__)
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.anonymous_user = AnonymousUserModel
bp = Blueprint('auth', __name__, url_prefix='/auth') def get_login_manager(root: RootConfigDirectoryContext) -> LoginManager:
"""
Creates a login manager
"""
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.anonymous_user = AnonymousUserModel
@login_manager.user_loader @login_manager.user_loader
def load_user(uid): def load_user(uid):
return UserModel.by(uid=str(uid)) return current_app.config['model_factory'].user(str(uid))
@bp.route('/login/', methods=['GET', 'POST']) return login_manager
bp_auth = Blueprint('auth', __name__, url_prefix='/auth')
@bp_auth.route('/login/', methods=['GET', 'POST'])
def login(): def login():
model_factory: ModelFactory = current_app.config['model_factory']
form = LoginForm() form = LoginForm()
if form.validate_on_submit(): if form.validate_on_submit():
username = form.username.data username = form.username.data
u = UserModel.by(name=username) user = model_factory.try_user(username)
if u is not None and u.check_password(form.password.data): if user is not None and user.check_password(form.password.data):
remember_me = form.remember.data remember_me = form.remember.data
login_user(u, remember=remember_me) login_user(user, remember=remember_me)
with json_rw(u.config_path) as cfg: with user.ctx.edit_config() as cfg:
cfg.last_login = int(time.time()) cfg.last_login = int(time.time())
logger.info("Logged in user '{}' ({})".format( logger.info('Logged in user "{0.username}" ({0.uid})'
u.username, u.uid)) .format(user.cfg))
return redirect(url_for('home.home')) return redirect(url_for('home.home'))
flash("Login not recognized") flash("Login not recognized")
return render_template('auth/login.html', form=form) return render_template('auth/login.html', form=form)
@bp.route("/logout/", methods=['GET'])
@bp_auth.route("/logout/", methods=['GET'])
@login_required @login_required
def logout(): def logout():
logout_user() logout_user()

View File

@ -1,3 +1,4 @@
from flask import current_app
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import ( from wtforms import (
StringField, PasswordField, BooleanField, SubmitField, TextAreaField, StringField, PasswordField, BooleanField, SubmitField, TextAreaField,
@ -5,9 +6,10 @@ from wtforms import (
from wtforms.validators import DataRequired, ValidationError, Optional from wtforms.validators import DataRequired, ValidationError, Optional
from wtforms.widgets.html5 import NumberInput from wtforms.widgets.html5 import NumberInput
from amanuensis.config import root # from amanuensis.config import root
from amanuensis.lexicon.manage import add_character # from amanuensis.lexicon.manage import add_character
from amanuensis.user import UserModel # from amanuensis.user import UserModel
from amanuensis.config import RootConfigDirectoryContext
# Custom validators # Custom validators
@ -15,10 +17,13 @@ def user(exists=True):
template = 'User "{{}}" {}'.format( template = 'User "{{}}" {}'.format(
"not found" if exists else "already exists") "not found" if exists else "already exists")
should_exist = bool(exists) should_exist = bool(exists)
def validate_user(form, field): def validate_user(form, field):
with root.user.index() as index: root: RootConfigDirectoryContext = current_app.config['root']
with root.user.read_index() as index:
if (field.data in index.keys()) != should_exist: if (field.data in index.keys()) != should_exist:
raise ValidationError(template.format(field.data)) raise ValidationError(template.format(field.data))
return validate_user return validate_user
@ -26,17 +31,21 @@ def lexicon(exists=True):
template = 'Lexicon "{{}}" {}'.format( template = 'Lexicon "{{}}" {}'.format(
"not found" if exists else "already exists") "not found" if exists else "already exists")
should_exist = bool(exists) should_exist = bool(exists)
def validate_lexicon(form, field): def validate_lexicon(form, field):
with root.lexicon.index() as index: root: RootConfigDirectoryContext = current_app.config['root']
with root.lexicon.read_index() as index:
if (field.data in index.keys()) != should_exist: if (field.data in index.keys()) != should_exist:
raise ValidationError(template.format(field.data)) raise ValidationError(template.format(field.data))
return validate_lexicon return validate_lexicon
# Forms # Forms
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
"""/auth/login/""" """/auth/login/"""
username = StringField('Username', validators=[DataRequired(), user(exists=True)]) username = StringField('Username',
validators=[DataRequired(), user(exists=True)])
password = PasswordField('Password', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()])
remember = BooleanField('Stay logged in') remember = BooleanField('Stay logged in')
submit = SubmitField('Log in') submit = SubmitField('Log in')
@ -54,194 +63,199 @@ class LexiconCreateForm(FlaskForm):
submit = SubmitField('Create') submit = SubmitField('Create')
class LexiconConfigForm(FlaskForm): # class LexiconConfigForm(FlaskForm):
"""/lexicon/<name>/session/settings/""" # """/lexicon/<name>/session/settings/"""
# General # # General
title = StringField('Title override', validators=[Optional()]) # title = StringField('Title override', validators=[Optional()])
editor = SelectField('Editor', validators=[DataRequired(), user(exists=True)]) # editor = SelectField('Editor', validators=[DataRequired(), user(exists=True)])
prompt = TextAreaField('Prompt', validators=[DataRequired()]) # prompt = TextAreaField('Prompt', validators=[DataRequired()])
# Turn # # Turn
turnCurrent = IntegerField('Current turn', widget=NumberInput(), validators=[Optional()]) # turnCurrent = IntegerField('Current turn', widget=NumberInput(), validators=[Optional()])
turnMax = IntegerField('Number of turns', widget=NumberInput(), validators=[DataRequired()]) # turnMax = IntegerField('Number of turns', widget=NumberInput(), validators=[DataRequired()])
# Join # # Join
joinPublic = BooleanField("Show game on public pages") # joinPublic = BooleanField("Show game on public pages")
joinOpen = BooleanField("Allow players to join game") # joinOpen = BooleanField("Allow players to join game")
joinPassword = StringField("Password to join game", validators=[Optional()]) # joinPassword = StringField("Password to join game", validators=[Optional()])
joinMaxPlayers = IntegerField( # joinMaxPlayers = IntegerField(
"Maximum number of players", # "Maximum number of players",
widget=NumberInput(), # widget=NumberInput(),
validators=[DataRequired()]) # validators=[DataRequired()])
joinCharsPerPlayer = IntegerField( # joinCharsPerPlayer = IntegerField(
"Characters per player", # "Characters per player",
widget=NumberInput(), # widget=NumberInput(),
validators=[DataRequired()]) # validators=[DataRequired()])
# Publish # # Publish
publishNotifyEditorOnReady = BooleanField( # publishNotifyEditorOnReady = BooleanField(
"Notify the editor when a player marks an article as ready") # "Notify the editor when a player marks an article as ready")
publishNotifyPlayerOnReject = BooleanField( # publishNotifyPlayerOnReject = BooleanField(
"Notify a player when their article is rejected by the editor") # "Notify a player when their article is rejected by the editor")
publishNotifyPlayerOnAccept = BooleanField( # publishNotifyPlayerOnAccept = BooleanField(
"Notify a player when their article is accepted by the editor") # "Notify a player when their article is accepted by the editor")
publishDeadlines = StringField( # publishDeadlines = StringField(
"Turn deadline, as a crontab specification", validators=[Optional()]) # "Turn deadline, as a crontab specification", validators=[Optional()])
publishAsap = BooleanField( # publishAsap = BooleanField(
"Publish the turn immediately when the last article is accepted") # "Publish the turn immediately when the last article is accepted")
publishQuorum = IntegerField( # publishQuorum = IntegerField(
"Quorum to publish incomplete turn", widget=NumberInput(), validators=[Optional()]) # "Quorum to publish incomplete turn", widget=NumberInput(), validators=[Optional()])
publishBlockOnReady = BooleanField( # publishBlockOnReady = BooleanField(
"Block turn publish if any articles are awaiting editor review") # "Block turn publish if any articles are awaiting editor review")
# Article # # Article
articleIndexList = TextAreaField("Index specifications") # articleIndexList = TextAreaField("Index specifications")
articleIndexCapacity = IntegerField( # articleIndexCapacity = IntegerField(
"Index capacity override", widget=NumberInput(), validators=[Optional()]) # "Index capacity override", widget=NumberInput(), validators=[Optional()])
articleCitationAllowSelf = BooleanField( # articleCitationAllowSelf = BooleanField(
"Allow players to cite themselves") # "Allow players to cite themselves")
articleCitationMinExtant = IntegerField( # articleCitationMinExtant = IntegerField(
"Minimum number of extant articles to cite", widget=NumberInput(), validators=[Optional()]) # "Minimum number of extant articles to cite", widget=NumberInput(), validators=[Optional()])
articleCitationMaxExtant = IntegerField( # articleCitationMaxExtant = IntegerField(
"Maximum number of extant articles to cite", widget=NumberInput(), validators=[Optional()]) # "Maximum number of extant articles to cite", widget=NumberInput(), validators=[Optional()])
articleCitationMinPhantom = IntegerField( # articleCitationMinPhantom = IntegerField(
"Minimum number of phantom articles to cite", widget=NumberInput(), validators=[Optional()]) # "Minimum number of phantom articles to cite", widget=NumberInput(), validators=[Optional()])
articleCitationMaxPhantom = IntegerField( # articleCitationMaxPhantom = IntegerField(
"Maximum number of phantom articles to cite", widget=NumberInput(), validators=[Optional()]) # "Maximum number of phantom articles to cite", widget=NumberInput(), validators=[Optional()])
articleCitationMinTotal = IntegerField( # articleCitationMinTotal = IntegerField(
"Minimum number of articles to cite in total", widget=NumberInput(), validators=[Optional()]) # "Minimum number of articles to cite in total", widget=NumberInput(), validators=[Optional()])
articleCitationMaxTotal = IntegerField( # articleCitationMaxTotal = IntegerField(
"Maximum number of articles to cite in total", widget=NumberInput(), validators=[Optional()]) # "Maximum number of articles to cite in total", widget=NumberInput(), validators=[Optional()])
articleCitationMinChars = IntegerField( # articleCitationMinChars = IntegerField(
"Minimum number of characters to cite articles by", # "Minimum number of characters to cite articles by",
widget=NumberInput(), validators=[Optional()]) # widget=NumberInput(), validators=[Optional()])
articleCitationMaxChars = IntegerField( # articleCitationMaxChars = IntegerField(
"Maximum number of characters to cite articles by", # "Maximum number of characters to cite articles by",
widget=NumberInput(), validators=[Optional()]) # widget=NumberInput(), validators=[Optional()])
articleWordLimitSoft = IntegerField( # articleWordLimitSoft = IntegerField(
"Soft word limit", widget=NumberInput(), validators=[Optional()]) # "Soft word limit", widget=NumberInput(), validators=[Optional()])
articleWordLimitHard = IntegerField( # articleWordLimitHard = IntegerField(
"Hard word limit", widget=NumberInput(), validators=[Optional()]) # "Hard word limit", widget=NumberInput(), validators=[Optional()])
articleAddendumAllowed = BooleanField("Allow addendum articles") # articleAddendumAllowed = BooleanField("Allow addendum articles")
articleAddendumMax = IntegerField( # articleAddendumMax = IntegerField(
"Maximum number of addendum articles per character per turn", # "Maximum number of addendum articles per character per turn",
widget=NumberInput(), validators=[Optional()]) # widget=NumberInput(), validators=[Optional()])
# And finally, the submit button # # And finally, the submit button
submit = SubmitField("Submit") # submit = SubmitField("Submit")
def validate_publishDeadlines(form, field): # def validate_publishDeadlines(form, field):
if form.publishAsap.data: # if form.publishAsap.data:
raise ValidationError('Cannot specify deadline if immediate publishing is enabled') # raise ValidationError('Cannot specify deadline if immediate publishing is enabled')
# TODO add validators that call into extant valid check methods # # TODO add validators that call into extant valid check methods
def set_options(self, lexicon): # def set_options(self, lexicon):
self.editor.choices = list(map(lambda x: (x, x), map( # self.editor.choices = list(map(lambda x: (x, x), map(
lambda uid: UserModel.by(uid=uid).username, # lambda uid: UserModel.by(uid=uid).username,
lexicon.join.joined))) # lexicon.join.joined)))
def populate_from_lexicon(self, lexicon): # def populate_from_lexicon(self, lexicon):
self.title.data = lexicon.title # self.title.data = lexicon.title
self.editor.data = UserModel.by(uid=lexicon.editor).username # self.editor.data = UserModel.by(uid=lexicon.editor).username
self.prompt.data = lexicon.prompt # self.prompt.data = lexicon.prompt
self.turnCurrent.data = lexicon.turn.current # self.turnCurrent.data = lexicon.turn.current
self.turnMax.data = lexicon.turn.max # self.turnMax.data = lexicon.turn.max
self.joinPublic.data = lexicon.join.public # self.joinPublic.data = lexicon.join.public
self.joinOpen.data = lexicon.join.open # self.joinOpen.data = lexicon.join.open
self.joinPassword.data = lexicon.join.password # self.joinPassword.data = lexicon.join.password
self.joinMaxPlayers.data = lexicon.join.max_players # self.joinMaxPlayers.data = lexicon.join.max_players
self.joinCharsPerPlayer.data = lexicon.join.chars_per_player # self.joinCharsPerPlayer.data = lexicon.join.chars_per_player
self.publishNotifyEditorOnReady.data = lexicon.publish.notify.editor_on_ready # self.publishNotifyEditorOnReady.data = lexicon.publish.notify.editor_on_ready
self.publishNotifyPlayerOnReject.data = lexicon.publish.notify.player_on_reject # self.publishNotifyPlayerOnReject.data = lexicon.publish.notify.player_on_reject
self.publishNotifyPlayerOnAccept.data = lexicon.publish.notify.player_on_accept # self.publishNotifyPlayerOnAccept.data = lexicon.publish.notify.player_on_accept
self.publishDeadlines.data = lexicon.publish.deadlines # self.publishDeadlines.data = lexicon.publish.deadlines
self.publishAsap.data = lexicon.publish.asap # self.publishAsap.data = lexicon.publish.asap
self.publishQuorum.data = lexicon.publish.quorum # self.publishQuorum.data = lexicon.publish.quorum
self.publishBlockOnReady.data = lexicon.publish.block_on_ready # self.publishBlockOnReady.data = lexicon.publish.block_on_ready
self.articleIndexList.data = lexicon.article.index.list # self.articleIndexList.data = lexicon.article.index.list
self.articleIndexCapacity.data = lexicon.article.index.capacity # self.articleIndexCapacity.data = lexicon.article.index.capacity
self.articleCitationAllowSelf.data = lexicon.article.citation.allow_self # self.articleCitationAllowSelf.data = lexicon.article.citation.allow_self
self.articleCitationMinExtant.data = lexicon.article.citation.min_extant # self.articleCitationMinExtant.data = lexicon.article.citation.min_extant
self.articleCitationMaxExtant.data = lexicon.article.citation.max_extant # self.articleCitationMaxExtant.data = lexicon.article.citation.max_extant
self.articleCitationMinPhantom.data = lexicon.article.citation.min_phantom # self.articleCitationMinPhantom.data = lexicon.article.citation.min_phantom
self.articleCitationMaxPhantom.data = lexicon.article.citation.max_phantom # self.articleCitationMaxPhantom.data = lexicon.article.citation.max_phantom
self.articleCitationMinTotal.data = lexicon.article.citation.min_total # self.articleCitationMinTotal.data = lexicon.article.citation.min_total
self.articleCitationMaxTotal.data = lexicon.article.citation.max_total # self.articleCitationMaxTotal.data = lexicon.article.citation.max_total
self.articleCitationMinChars.data = lexicon.article.citation.min_chars # self.articleCitationMinChars.data = lexicon.article.citation.min_chars
self.articleCitationMaxChars.data = lexicon.article.citation.max_chars # self.articleCitationMaxChars.data = lexicon.article.citation.max_chars
self.articleWordLimitSoft.data = lexicon.article.word_limit.soft # self.articleWordLimitSoft.data = lexicon.article.word_limit.soft
self.articleWordLimitHard.data = lexicon.article.word_limit.hard # self.articleWordLimitHard.data = lexicon.article.word_limit.hard
self.articleAddendumAllowed.data = lexicon.article.addendum.allowed # self.articleAddendumAllowed.data = lexicon.article.addendum.allowed
self.articleAddendumMax.data = lexicon.article.addendum.max # self.articleAddendumMax.data = lexicon.article.addendum.max
def update_lexicon(self, lexicon): # def update_lexicon(self, lexicon):
with lexicon.edit() as l: # with lexicon.edit() as l:
l.title = self.title.data # l.title = self.title.data
l.editor = UserModel.by(name=self.editor.data).uid # l.editor = UserModel.by(name=self.editor.data).uid
l.prompt = self.prompt.data # l.prompt = self.prompt.data
l.turn.current = self.turnCurrent.data # l.turn.current = self.turnCurrent.data
l.turn.max = self.turnMax.data # l.turn.max = self.turnMax.data
l.join.public = self.joinPublic.data # l.join.public = self.joinPublic.data
l.join.open = self.joinOpen.data # l.join.open = self.joinOpen.data
l.join.password = self.joinPassword.data # l.join.password = self.joinPassword.data
l.join.max_players = self.joinMaxPlayers.data # l.join.max_players = self.joinMaxPlayers.data
l.join.chars_per_player = self.joinCharsPerPlayer.data # l.join.chars_per_player = self.joinCharsPerPlayer.data
l.publish.notify.editor_on_ready = self.publishNotifyEditorOnReady.data # l.publish.notify.editor_on_ready = self.publishNotifyEditorOnReady.data
l.publish.notify.player_on_reject = self.publishNotifyPlayerOnReject.data # l.publish.notify.player_on_reject = self.publishNotifyPlayerOnReject.data
l.publish.notify.player_on_accept = self.publishNotifyPlayerOnAccept.data # l.publish.notify.player_on_accept = self.publishNotifyPlayerOnAccept.data
l.publish.deadlines = self.publishDeadlines.data # l.publish.deadlines = self.publishDeadlines.data
l.publish.asap = self.publishAsap.data # l.publish.asap = self.publishAsap.data
l.publish.quorum = self.publishQuorum.data # l.publish.quorum = self.publishQuorum.data
l.publish.block_on_ready = self.publishBlockOnReady.data # l.publish.block_on_ready = self.publishBlockOnReady.data
l.article.index.list = self.articleIndexList.data # l.article.index.list = self.articleIndexList.data
l.article.index.capacity = self.articleIndexCapacity.data # l.article.index.capacity = self.articleIndexCapacity.data
l.article.citation.allow_self = self.articleCitationAllowSelf.data # l.article.citation.allow_self = self.articleCitationAllowSelf.data
l.article.citation.min_extant = self.articleCitationMinExtant.data # l.article.citation.min_extant = self.articleCitationMinExtant.data
l.article.citation.max_extant = self.articleCitationMaxExtant.data # l.article.citation.max_extant = self.articleCitationMaxExtant.data
l.article.citation.min_phantom = self.articleCitationMinPhantom.data # l.article.citation.min_phantom = self.articleCitationMinPhantom.data
l.article.citation.max_phantom = self.articleCitationMaxPhantom.data # l.article.citation.max_phantom = self.articleCitationMaxPhantom.data
l.article.citation.min_total = self.articleCitationMinTotal.data # l.article.citation.min_total = self.articleCitationMinTotal.data
l.article.citation.max_total = self.articleCitationMaxTotal.data # l.article.citation.max_total = self.articleCitationMaxTotal.data
l.article.citation.min_chars = self.articleCitationMinChars.data # l.article.citation.min_chars = self.articleCitationMinChars.data
l.article.citation.max_chars = self.articleCitationMaxChars.data # l.article.citation.max_chars = self.articleCitationMaxChars.data
l.article.word_limit.soft = self.articleWordLimitSoft.data # l.article.word_limit.soft = self.articleWordLimitSoft.data
l.article.word_limit.hard = self.articleWordLimitHard.data # l.article.word_limit.hard = self.articleWordLimitHard.data
l.article.addendum.allowed = self.articleAddendumAllowed.data # l.article.addendum.allowed = self.articleAddendumAllowed.data
l.article.addendum.max = self.articleAddendumMax.data # l.article.addendum.max = self.articleAddendumMax.data
return True # return True
class LexiconJoinForm(FlaskForm): # class LexiconJoinForm(FlaskForm):
"""/lexicon/<name>/join/""" # """/lexicon/<name>/join/"""
password = StringField('Password') # password = StringField('Password')
submit = SubmitField("Submit") # submit = SubmitField("Submit")
class LexiconCharacterForm(FlaskForm): # class LexiconCharacterForm(FlaskForm):
"""/lexicon/<name>/session/character/""" # """/lexicon/<name>/session/character/"""
characterName = StringField("Character name", validators=[DataRequired()]) # characterName = StringField("Character name", validators=[DataRequired()])
defaultSignature = TextAreaField("Default signature") # defaultSignature = TextAreaField("Default signature")
submit = SubmitField("Submit") # submit = SubmitField("Submit")
def for_new(self): # def for_new(self):
self.characterName.data = "" # self.characterName.data = ""
self.defaultSignature.data = "~" # self.defaultSignature.data = "~"
def for_character(self, lexicon, cid): # def for_character(self, lexicon, cid):
char = lexicon.character.get(cid) # char = lexicon.character.get(cid)
self.characterName.data = char.name # self.characterName.data = char.name
self.defaultSignature.data = char.signature # self.defaultSignature.data = char.signature
def add_character(self, lexicon, user): # def add_character(self, lexicon, user):
add_character(lexicon, user, { # add_character(lexicon, user, {
'name': self.characterName.data, # 'name': self.characterName.data,
'signature': self.defaultSignature.data, # 'signature': self.defaultSignature.data,
}) # })
def update_character(self, lexicon, cid): # def update_character(self, lexicon, cid):
with lexicon.edit() as l: # with lexicon.edit() as l:
char = l.character.get(cid) # char = l.character.get(cid)
char.name = self.characterName.data # char.name = self.characterName.data
char.signature = self.defaultSignature.data # char.signature = self.defaultSignature.data
class LexiconReviewForm(FlaskForm): # class LexiconReviewForm(FlaskForm):
"""/lexicon/<name>/session/review/""" # """/lexicon/<name>/session/review/"""
approved = RadioField("Buttons", choices=(("Y", "Approved"), ("N", "Rejected"))) # approved = RadioField("Buttons", choices=(("Y", "Approved"), ("N", "Rejected")))
submit = SubmitField("Submit") # submit = SubmitField("Submit")
# class LexiconPublishTurnForm(FlaskForm):
# """/lexicon/<name>/session/"""
# submit = SubmitField("Publish turn")

View File

@ -7,18 +7,18 @@ from flask import g, flash, redirect, url_for, current_app
from flask_login import current_user from flask_login import current_user
# Module imports # Module imports
from amanuensis.lexicon import LexiconModel
from amanuensis.parser import filesafe_title from amanuensis.parser import filesafe_title
from amanuensis.user import UserModel from amanuensis.models import ModelFactory, UserModel
from amanuensis.models import ModelFactory
def register_custom_filters(app): def register_custom_filters(app):
"""Adds custom filters to the Flask app""" """Adds custom filters to the Flask app"""
@app.template_filter("user_attr") @app.template_filter("user_attr")
def get_user_attr(uid, attr): def get_user_attr(uid, attr):
user = UserModel.by(uid=uid) factory: ModelFactory = current_app.config['model_factory']
val = getattr(user, attr) user: UserModel = factory.user(uid)
val = getattr(user.cfg, attr)
return val return val
@app.template_filter("asdate") @app.template_filter("asdate")
@ -30,23 +30,26 @@ def register_custom_filters(app):
@app.template_filter("articlelink") @app.template_filter("articlelink")
def article_link(title): def article_link(title):
return url_for('lexicon.article', name=g.lexicon.name, title=filesafe_title(title)) return url_for(
'lexicon.article',
name=g.lexicon.name,
title=filesafe_title(title))
def lexicon_param(route): # def lexicon_param(route):
"""Wrapper for loading a route's lexicon""" # """Wrapper for loading a route's lexicon"""
@wraps(route) # @wraps(route)
def with_lexicon(**kwargs): # def with_lexicon(**kwargs):
name = kwargs.get('name') # name = kwargs.get('name')
g.lexicon = LexiconModel.by(name=name) # g.lexicon = LexiconModel.by(name=name)
if g.lexicon is None: # if g.lexicon is None:
flash("Couldn't find a lexicon with the name '{}'".format(name)) # flash("Couldn't find a lexicon with the name '{}'".format(name))
return redirect(url_for("home.home")) # return redirect(url_for("home.home"))
# TODO transition to new model # # TODO transition to new model
model_factory: ModelFactory = current_app.config['model_factory'] # model_factory: ModelFactory = current_app.config['model_factory']
g.lexicon_ = model_factory.lexicon(name) # g.lexicon_ = model_factory.lexicon(name)
return route(**kwargs) # return route(**kwargs)
return with_lexicon # return with_lexicon
def admin_required(route): def admin_required(route):
@ -55,7 +58,7 @@ def admin_required(route):
""" """
@wraps(route) @wraps(route)
def admin_route(*args, **kwargs): def admin_route(*args, **kwargs):
if not current_user.is_admin: if not current_user.cfg.is_admin:
flash("You must be an admin to view this page") flash("You must be an admin to view this page")
return redirect(url_for('home.home')) return redirect(url_for('home.home'))
return route(*args, **kwargs) return route(*args, **kwargs)
@ -68,10 +71,10 @@ def player_required(route):
""" """
@wraps(route) @wraps(route)
def player_route(*args, **kwargs): def player_route(*args, **kwargs):
if not current_user.in_lexicon(g.lexicon): if current_user.uid not in g.lexicon.cfg.join.joined:
flash("You must be a player to view this page") flash("You must be a player to view this page")
return (redirect(url_for('lexicon.contents', name=g.lexicon.name)) return (redirect(url_for('lexicon.contents', name=g.lexicon.cfg.name))
if g.lexicon.join.public if g.lexicon.cfg.join.public
else redirect(url_for('home.home'))) else redirect(url_for('home.home')))
return route(*args, **kwargs) return route(*args, **kwargs)
return player_route return player_route
@ -84,8 +87,8 @@ def player_required_if_not_public(route):
""" """
@wraps(route) @wraps(route)
def player_route(*args, **kwargs): def player_route(*args, **kwargs):
if ((not g.lexicon.join.public) if ((not g.lexicon.cfg.join.public)
and not current_user.in_lexicon(g.lexicon)): and current_user.uid not in g.lexicon.cfg.join.joined):
flash("You must be a player to view this page") flash("You must be a player to view this page")
return redirect(url_for('home.home')) return redirect(url_for('home.home'))
return route(*args, **kwargs) return route(*args, **kwargs)
@ -99,7 +102,7 @@ def editor_required(route):
""" """
@wraps(route) @wraps(route)
def editor_route(*args, **kwargs): def editor_route(*args, **kwargs):
if current_user.id != g.lexicon.editor: if current_user.uid != g.lexicon.cfg.editor:
flash("You must be the editor to view this page") flash("You must be the editor to view this page")
return redirect(url_for('lexicon.contents', name=g.lexicon.name)) return redirect(url_for('lexicon.contents', name=g.lexicon.name))
return route(*args, **kwargs) return route(*args, **kwargs)

View File

@ -1,64 +1,58 @@
from flask import Blueprint, render_template, redirect, url_for from flask import Blueprint, render_template, redirect, url_for, current_app
from flask_login import login_required, current_user from flask_login import login_required, current_user
from amanuensis.config import json_ro, json_rw from amanuensis.config import RootConfigDirectoryContext
from amanuensis.lexicon import LexiconModel from amanuensis.lexicon import create_lexicon, load_all_lexicons
from amanuensis.lexicon.manage import create_lexicon, get_all_lexicons from amanuensis.models import UserModel
from amanuensis.server.forms import LexiconCreateForm from amanuensis.server.forms import LexiconCreateForm
from amanuensis.server.helpers import admin_required from amanuensis.server.helpers import admin_required
from amanuensis.user import UserModel from amanuensis.user import load_all_users
def get_bp(): bp_home = Blueprint('home', __name__, url_prefix='/home')
"""Create a blueprint for pages outside of a specific lexicon"""
bp = Blueprint('home', __name__, url_prefix='/home')
@bp.route('/', methods=['GET'])
def home():
user_lexicons = []
public_lexicons = []
for lexicon in get_all_lexicons():
if current_user.in_lexicon(lexicon):
user_lexicons.append(lexicon)
elif lexicon.join.public:
public_lexicons.append(lexicon)
return render_template(
'home/home.html',
user_lexicons=user_lexicons,
public_lexicons=public_lexicons)
@bp.route('/admin/', methods=['GET']) @bp_home.route('/', methods=['GET'])
@login_required def home():
@admin_required root: RootConfigDirectoryContext = current_app.config['root']
def admin(): user: UserModel = current_user
users = [] user_lexicons = []
with json_ro('user', 'index.json') as index: public_lexicons = []
for name, uid in index.items(): for lexicon in load_all_lexicons(root):
users.append(UserModel.by(uid=uid)) if user.uid in lexicon.cfg.join.joined:
user_lexicons.append(lexicon)
elif lexicon.cfg.join.public:
public_lexicons.append(lexicon)
return render_template(
'home/home.html',
user_lexicons=user_lexicons,
public_lexicons=public_lexicons)
lexicons = []
with json_ro('lexicon', 'index.json') as index:
for name, lid in index.items():
lexicons.append(LexiconModel.by(lid=lid))
return render_template('home/admin.html', users=users, lexicons=lexicons) @bp_home.route('/admin/', methods=['GET'])
@login_required
@admin_required
def admin():
root: RootConfigDirectoryContext = current_app.config['root']
users = list(load_all_users(root))
lexicons = list(load_all_lexicons(root))
return render_template('home/admin.html', users=users, lexicons=lexicons)
@bp.route("/admin/create/", methods=['GET', 'POST'])
@login_required
@admin_required
def admin_create():
form = LexiconCreateForm()
if form.validate_on_submit(): @bp_home.route("/admin/create/", methods=['GET', 'POST'])
lexicon_name = form.lexiconName.data @login_required
editor_name = form.editorName.data @admin_required
prompt = form.promptText.data def admin_create():
editor = UserModel.by(name=editor_name) form = LexiconCreateForm()
lexicon = create_lexicon(lexicon_name, editor)
with json_rw(lexicon.config_path) as cfg:
cfg.prompt = prompt
return redirect(url_for('lexicon.session', name=lexicon_name))
return render_template('home/create.html', form=form) if form.validate_on_submit():
lexicon_name = form.lexiconName.data
editor_name = form.editorName.data
prompt = form.promptText.data
editor = UserModel.by(name=editor_name)
lexicon = create_lexicon(lexicon_name, editor)
with lexicon.ctx.edit_config() as cfg:
cfg.prompt = prompt
return redirect(url_for('lexicon.session', name=lexicon_name))
return bp return render_template('home/create.html', form=form)

View File

@ -34,7 +34,7 @@
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %} {% set template_content_blocks = [self.main()] %}
{% if current_user.is_admin %} {% if current_user.cfg.is_admin %}
{% block admin_dash %} {% block admin_dash %}
<a href="{{ url_for('home.admin') }}" style="display:block; text-align:center;">Admin dashboard</a> <a href="{{ url_for('home.admin') }}" style="display:block; text-align:center;">Admin dashboard</a>
{% endblock %} {% endblock %}

View File

@ -1,31 +1,31 @@
{% macro dashboard_lexicon_item(lexicon) %} {% macro dashboard_lexicon_item(lexicon) %}
<div class="dashboard-lexicon-item dashboard-lexicon-{{ lexicon.status() }}"> <div class="dashboard-lexicon-item dashboard-lexicon-{{ lexicon.status }}">
<p> <p>
<span class="dashboard-lexicon-item-title"> <span class="dashboard-lexicon-item-title">
<a href="{{ url_for('lexicon.contents', name=lexicon.name) }}"> {# <a href="{{ url_for('lexicon.contents', name=lexicon.cfg.name) }}"> #}
Lexicon {{ lexicon.name }}</a> Lexicon {{ lexicon.cfg.name }}{# </a> #}
</span> </span>
[{{ lexicon.status().capitalize() }}] [{{ lexicon.status.capitalize() }}]
</p> </p>
<p><i>{{ lexicon.prompt }}</i></p> <p><i>{{ lexicon.cfg.prompt }}</i></p>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<p> <p>
{% {%
if current_user.in_lexicon(lexicon) if current_user.uid in lexicon.cfg.join.joined
or current_user.is_admin or current_user.cfg.is_admin
%} %}
Editor: {{ lexicon.editor|user_attr('username') }} / Editor: {{ lexicon.cfg.editor|user_attr('username') }} /
Players: Players:
{% for uid in lexicon.join.joined %} {% for uid in lexicon.cfg.join.joined %}
{{ uid|user_attr('username') }}{% if not loop.last %}, {% endif %} {{ uid|user_attr('username') }}{% if not loop.last %}, {% endif %}
{% endfor %} {% endfor %}
({{ lexicon.join.joined|count }}/{{ lexicon.join.max_players }}) ({{ lexicon.cfg.join.joined|count }}/{{ lexicon.cfg.join.max_players }})
{% else %} {% else %}
Players: {{ lexicon.join.joined|count }}/{{ lexicon.join.max_players }} Players: {{ lexicon.cfg.join.joined|count }}/{{ lexicon.cfg.join.max_players }}
{% if lexicon.join.public and lexicon.join.open %} {% if lexicon.cfg.join.public and lexicon.cfg.join.open %}
/ <a href="{{ url_for('lexicon.join', name=lexicon.name) }}"> {# / <a href="{{ url_for('lexicon.join', name=lexicon.cfg.name) }}"> #}
Join game Join game
</a> {# </a> #}
{% endif %} {% endif %}
{% endif %} {% endif %}
</p> </p>
@ -36,10 +36,10 @@
{% macro dashboard_user_item(user) %} {% macro dashboard_user_item(user) %}
<div class="dashboard-lexicon-item"> <div class="dashboard-lexicon-item">
<p> <p>
<b>{{ user.username }}</b> <b>{{ user.cfg.username }}</b>
{% if user.username != user.displayname %} / {{ user.displayname }}{% endif %} {% if user.cfg.username != user.cfg.displayname %} / {{ user.cfg.displayname }}{% endif %}
({{user.uid}}) ({{user.uid}})
</p> </p>
<p>Last activity: {{ user.last_activity|asdate }} &mdash; Last login: {{ user.last_login|asdate }}</p> <p>Last activity: {{ user.cfg.last_activity|asdate }} &mdash; Last login: {{ user.cfg.last_login|asdate }}</p>
</div> </div>
{% endmacro %} {% endmacro %}

View File

@ -11,7 +11,7 @@
<div id="header"> <div id="header">
<div id="login-status" {% block login_status_attr %}{% endblock %}> <div id="login-status" {% block login_status_attr %}{% endblock %}>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<b>{{ current_user.username -}}</b> <b>{{ current_user.cfg.username -}}</b>
(<a href="{{ url_for('auth.logout') }}">Logout</a>) (<a href="{{ url_for('auth.logout') }}">Logout</a>)
{% else %} {% else %}
<a href="{{ url_for('auth.login') }}">Login</a> <a href="{{ url_for('auth.login') }}">Login</a>