Rewrite settings page to reduce boilerplate

This commit is contained in:
Tim Van Baak 2020-04-29 17:13:12 -07:00
parent 7488a8ca79
commit 64b7ef0dee
3 changed files with 285 additions and 163 deletions

View File

@ -26,8 +26,8 @@ from amanuensis.server.helpers import (
from .forms import (
LexiconCharacterForm,
LexiconReviewForm,
LexiconPublishTurnForm)
from .settings import LexiconConfigForm
LexiconPublishTurnForm,
LexiconConfigForm)
from .editor import load_editor, new_draft, update_draft
@ -142,24 +142,23 @@ def character(name):
@lexicon_param
@editor_required
def settings(name):
form = LexiconConfigForm()
form.set_options(g.lexicon)
form: LexiconConfigForm = LexiconConfigForm(g.lexicon)
# Load the config for the lexicon on load
if not form.is_submitted():
form.populate_from_lexicon(g.lexicon)
# GET
form.load(g.lexicon)
return render_template('session.settings.jinja', form=form)
if form.validate():
if not form.update_lexicon(g.lexicon):
flash("Error updating settings")
return render_template("lexicon.settings.jinja", form=form)
flash("Settings updated")
return redirect(url_for('session.session', name=name))
flash("Validation error")
if not form.validate():
# POST with invalid data
flash('Validation error')
return render_template('session.settings.jinja', form=form)
# POST with valid data
form.save(g.lexicon)
flash('Settings updated')
return redirect(url_for('session.settings', name=name))
@bp_session.route('/review/', methods=['GET', 'POST'])
@lexicon_param

View File

@ -3,6 +3,8 @@ from wtforms import (
StringField, SubmitField, TextAreaField, RadioField)
from wtforms.validators import DataRequired
from .settings import ConfigFormBase
class LexiconCharacterForm(FlaskForm):
"""/lexicon/<name>/session/character/"""
@ -36,3 +38,8 @@ class LexiconReviewForm(FlaskForm):
class LexiconPublishTurnForm(FlaskForm):
"""/lexicon/<name>/session/"""
submit = SubmitField('Publish turn')
class LexiconConfigForm(ConfigFormBase):
"""/lexicon/<name>/session/settings/"""
submit = SubmitField('Save settings')

View File

@ -1,161 +1,277 @@
from typing import cast
from flask import current_app
from flask_wtf import FlaskForm
from wtforms import (
StringField, BooleanField, SubmitField, TextAreaField,
IntegerField, SelectField)
from wtforms.validators import DataRequired, ValidationError, Optional
Field,
StringField,
BooleanField,
TextAreaField,
IntegerField,
SelectField)
from wtforms.validators import DataRequired, Optional
from wtforms.widgets.html5 import NumberInput
from amanuensis.config import ReadOnlyOrderedDict, AttrOrderedDict
from amanuensis.models import ModelFactory, UserModel
from amanuensis.server.forms import User
class LexiconConfigForm(FlaskForm):
"""/lexicon/<name>/session/settings/"""
# General
title = StringField('Title override', validators=[Optional()])
editor = SelectField('Editor', validators=[DataRequired(), User(True)])
prompt = TextAreaField('Prompt', validators=[DataRequired()])
# Turn
turnCurrent = IntegerField('Current turn', widget=NumberInput(), validators=[Optional()])
turnMax = IntegerField('Number of turns', widget=NumberInput(), validators=[DataRequired()])
# Join
joinPublic = BooleanField("Show game on public pages")
joinOpen = BooleanField("Allow players to join game")
joinPassword = StringField("Password to join game", validators=[Optional()])
joinMaxPlayers = IntegerField(
"Maximum number of players",
class SettingTranslator():
"""
Base class for the translation layer between internal config data
and user-friendly display in the settings form. By default the data
is returned as-is.
"""
def load(self, cfg_value):
return cfg_value
def save(self, field_data):
return field_data
class UsernameTranslator(SettingTranslator):
"""
Converts an internal user id to a public-facing username.
"""
def load(self, cfg_value):
model_factory: ModelFactory = current_app.config['model_factory']
user: UserModel = model_factory.user(cfg_value)
return user.cfg.username
def save(self, field_data):
model_factory: ModelFactory = current_app.config['model_factory']
user: UserModel = model_factory.try_user(field_data)
if user:
return user.uid
class Setting():
"""
Represents a relation between a node in a lexicon config and a
field in a public-facing form that exposes it to the editor for
modification.
"""
def __init__(
self,
cfg_key: str,
field: Field,
translator: SettingTranslator = SettingTranslator()):
"""
Creates a setting. Optionally, defines a nontrivial translation
between internal and public values.
"""
self.cfg_path = cfg_key.split('.')
self.field = field
self.translator = translator
def load(self, cfg: ReadOnlyOrderedDict, field: Field):
"""
Sets the field's value to the corresponding config node
"""
for key in self.cfg_path[:-1]:
cfg = cast(ReadOnlyOrderedDict, cfg.get(key))
data = cfg.get(self.cfg_path[-1])
field.data = self.translator.load(data)
def save(self, cfg: AttrOrderedDict, field: Field):
"""
Updates the editable config with this field's value
"""
for key in self.cfg_path[:-1]:
cfg = cast(AttrOrderedDict, cfg.get(key))
data = field.data
cfg[self.cfg_path[-1]] = self.translator.save(data)
class Settings():
@staticmethod
def settings():
for name, setting in vars(Settings).items():
if name.startswith('s_'):
yield name, setting
s_title = Setting('title',
StringField('Title override', validators=[Optional()]))
s_editor = Setting('editor',
SelectField('Editor', validators=[DataRequired(), User(True)]),
translator=UsernameTranslator())
s_prompt = Setting('prompt',
TextAreaField('Prompt', validators=[DataRequired()]))
s_turnCurrent = Setting('turn.current',
IntegerField(
'Current turn',
widget=NumberInput(),
validators=[DataRequired()])
joinCharsPerPlayer = IntegerField(
"Characters per player",
validators=[Optional()]))
s_turnMax = Setting('turn.max',
IntegerField(
'Number of turns',
widget=NumberInput(),
validators=[DataRequired()])
# Publish
publishNotifyEditorOnReady = BooleanField(
"Notify the editor when a player marks an article as ready")
publishNotifyPlayerOnReject = BooleanField(
"Notify a player when their article is rejected by the editor")
publishNotifyPlayerOnAccept = BooleanField(
"Notify a player when their article is accepted by the editor")
publishDeadlines = StringField(
"Turn deadline, as a crontab specification", validators=[Optional()])
publishAsap = BooleanField(
"Publish the turn immediately when the last article is accepted")
publishQuorum = IntegerField(
"Quorum to publish incomplete turn", widget=NumberInput(), validators=[Optional()])
publishBlockOnReady = BooleanField(
"Block turn publish if any articles are awaiting editor review")
# Article
articleIndexList = TextAreaField("Index specifications")
articleIndexCapacity = IntegerField(
"Index capacity override", widget=NumberInput(), validators=[Optional()])
articleCitationAllowSelf = BooleanField(
"Allow players to cite themselves")
articleCitationMinExtant = IntegerField(
"Minimum number of extant articles to cite", widget=NumberInput(), validators=[Optional()])
articleCitationMaxExtant = IntegerField(
"Maximum number of extant articles to cite", widget=NumberInput(), validators=[Optional()])
articleCitationMinPhantom = IntegerField(
"Minimum number of phantom articles to cite", widget=NumberInput(), validators=[Optional()])
articleCitationMaxPhantom = IntegerField(
"Maximum number of phantom articles to cite", widget=NumberInput(), validators=[Optional()])
articleCitationMinTotal = IntegerField(
"Minimum number of articles to cite in total", widget=NumberInput(), validators=[Optional()])
articleCitationMaxTotal = IntegerField(
"Maximum number of articles to cite in total", widget=NumberInput(), validators=[Optional()])
articleCitationMinChars = IntegerField(
"Minimum number of characters to cite articles by",
widget=NumberInput(), validators=[Optional()])
articleCitationMaxChars = IntegerField(
"Maximum number of characters to cite articles by",
widget=NumberInput(), validators=[Optional()])
articleWordLimitSoft = IntegerField(
"Soft word limit", widget=NumberInput(), validators=[Optional()])
articleWordLimitHard = IntegerField(
"Hard word limit", widget=NumberInput(), validators=[Optional()])
articleAddendumAllowed = BooleanField("Allow addendum articles")
articleAddendumMax = IntegerField(
"Maximum number of addendum articles per character per turn",
widget=NumberInput(), validators=[Optional()])
# And finally, the submit button
submit = SubmitField("Submit")
validators=[DataRequired()]))
def validate_publishDeadlines(form, field):
if form.publishAsap.data:
raise ValidationError('Cannot specify deadline if immediate publishing is enabled')
s_joinPublic = Setting('join.public',
BooleanField('Show game on public pages'))
# TODO add validators that call into extant valid check methods
s_joinOpen = Setting('join.open',
BooleanField('Allow players to join game'))
# def set_options(self, lexicon):
# self.editor.choices = list(map(lambda x: (x, x), map(
# lambda uid: UserModel.by(uid=uid).username,
# lexicon.join.joined)))
s_joinPassword = Setting('join.password',
StringField('Password to join game', validators=[Optional()]))
# def populate_from_lexicon(self, lexicon):
# self.title.data = lexicon.cfg.title
# self.editor.data = ModelFactory(lexicon.ctx.root).user(lexicon.cfg.editor).cfg.username
# self.prompt.data = lexicon.prompt
# self.turnCurrent.data = lexicon.turn.current
# self.turnMax.data = lexicon.turn.max
# self.joinPublic.data = lexicon.join.public
# self.joinOpen.data = lexicon.join.open
# self.joinPassword.data = lexicon.join.password
# self.joinMaxPlayers.data = lexicon.join.max_players
# self.joinCharsPerPlayer.data = lexicon.join.chars_per_player
# self.publishNotifyEditorOnReady.data = lexicon.publish.notify.editor_on_ready
# self.publishNotifyPlayerOnReject.data = lexicon.publish.notify.player_on_reject
# self.publishNotifyPlayerOnAccept.data = lexicon.publish.notify.player_on_accept
# self.publishDeadlines.data = lexicon.publish.deadlines
# self.publishAsap.data = lexicon.publish.asap
# self.publishQuorum.data = lexicon.publish.quorum
# self.publishBlockOnReady.data = lexicon.publish.block_on_ready
# self.articleIndexList.data = lexicon.article.index.list
# self.articleIndexCapacity.data = lexicon.article.index.capacity
# self.articleCitationAllowSelf.data = lexicon.article.citation.allow_self
# self.articleCitationMinExtant.data = lexicon.article.citation.min_extant
# self.articleCitationMaxExtant.data = lexicon.article.citation.max_extant
# self.articleCitationMinPhantom.data = lexicon.article.citation.min_phantom
# self.articleCitationMaxPhantom.data = lexicon.article.citation.max_phantom
# self.articleCitationMinTotal.data = lexicon.article.citation.min_total
# self.articleCitationMaxTotal.data = lexicon.article.citation.max_total
# self.articleCitationMinChars.data = lexicon.article.citation.min_chars
# self.articleCitationMaxChars.data = lexicon.article.citation.max_chars
# self.articleWordLimitSoft.data = lexicon.article.word_limit.soft
# self.articleWordLimitHard.data = lexicon.article.word_limit.hard
# self.articleAddendumAllowed.data = lexicon.article.addendum.allowed
# self.articleAddendumMax.data = lexicon.article.addendum.max
s_joinMaxPlayers = Setting('join.max_players',
IntegerField(
'Maximum number of players',
widget=NumberInput(),
validators=[DataRequired()]))
# def update_lexicon(self, lexicon):
# with lexicon.edit() as l:
# l.title = self.title.data
# l.editor = UserModel.by(name=self.editor.data).uid
# l.prompt = self.prompt.data
# l.turn.current = self.turnCurrent.data
# l.turn.max = self.turnMax.data
# l.join.public = self.joinPublic.data
# l.join.open = self.joinOpen.data
# l.join.password = self.joinPassword.data
# l.join.max_players = self.joinMaxPlayers.data
# l.join.chars_per_player = self.joinCharsPerPlayer.data
# l.publish.notify.editor_on_ready = self.publishNotifyEditorOnReady.data
# l.publish.notify.player_on_reject = self.publishNotifyPlayerOnReject.data
# l.publish.notify.player_on_accept = self.publishNotifyPlayerOnAccept.data
# l.publish.deadlines = self.publishDeadlines.data
# l.publish.asap = self.publishAsap.data
# l.publish.quorum = self.publishQuorum.data
# l.publish.block_on_ready = self.publishBlockOnReady.data
# l.article.index.list = self.articleIndexList.data
# l.article.index.capacity = self.articleIndexCapacity.data
# l.article.citation.allow_self = self.articleCitationAllowSelf.data
# l.article.citation.min_extant = self.articleCitationMinExtant.data
# l.article.citation.max_extant = self.articleCitationMaxExtant.data
# l.article.citation.min_phantom = self.articleCitationMinPhantom.data
# l.article.citation.max_phantom = self.articleCitationMaxPhantom.data
# l.article.citation.min_total = self.articleCitationMinTotal.data
# l.article.citation.max_total = self.articleCitationMaxTotal.data
# l.article.citation.min_chars = self.articleCitationMinChars.data
# l.article.citation.max_chars = self.articleCitationMaxChars.data
# l.article.word_limit.soft = self.articleWordLimitSoft.data
# l.article.word_limit.hard = self.articleWordLimitHard.data
# l.article.addendum.allowed = self.articleAddendumAllowed.data
# l.article.addendum.max = self.articleAddendumMax.data
# return True
s_joinCharsPerPlayer = Setting('join.chars_per_player',
IntegerField(
'Characters per player',
widget=NumberInput(),
validators=[DataRequired()]))
s_publishNotifyEditorOnReady = Setting('publish.notify_editor_on_ready',
BooleanField(
'Notify the editor when a player marks an article as ready'))
s_publishNotifyPlayerOnReject = Setting('publish.notify_player_on_reject',
BooleanField(
'Notify a player when their article is rejected by the editor'))
s_publishNotifyPlayerOnAccept = Setting('publish.notify_player_on_accept',
BooleanField(
'Notify a player when their article is accepted by the editor'))
s_publishDeadlines = Setting('publish.deadlines',
StringField(
'Turn deadline, as a crontab specification',
validators=[Optional()]))
s_publishAsap = Setting('publish.asap',
BooleanField(
'Publish the turn immediately when the last article is accepted'))
s_publishQuorum = Setting('publish.quorum',
IntegerField(
'Quorum to publish incomplete turn',
widget=NumberInput(),
validators=[Optional()]))
s_publishBlockOnReady = Setting('publish.block_on_ready',
BooleanField(
'Block turn publish if any articles are awaiting editor review'))
s_articleIndexList = Setting('article.index.list',
TextAreaField('Index specifications'))
s_articleIndexCapacity = Setting('article.index.capacity',
IntegerField(
'Index capacity override',
widget=NumberInput(),
validators=[Optional()]))
s_articleCitationAllowSelf = Setting('article.citation.allow_self',
BooleanField('Allow players to cite themselves'))
s_articleCitationMinExtant = Setting('article.citation.min_extant',
IntegerField(
'Minimum number of extant articles to cite',
widget=NumberInput(),
validators=[Optional()]))
s_articleCitationMaxExtant = Setting('article.citation.max_extant',
IntegerField(
'Maximum number of extant articles to cite',
widget=NumberInput(),
validators=[Optional()]))
s_articleCitationMinPhantom = Setting('article.citation.min_phantom',
IntegerField(
'Minimum number of phantom articles to cite',
widget=NumberInput(),
validators=[Optional()]))
s_articleCitationMaxPhantom = Setting('article.citation.max_phantom',
IntegerField(
'Maximum number of phantom articles to cite',
widget=NumberInput(),
validators=[Optional()]))
s_articleCitationMinTotal = Setting('article.citation.min_total',
IntegerField(
'Minimum number of articles to cite in total',
widget=NumberInput(),
validators=[Optional()]))
s_articleCitationMaxTotal = Setting('article.citation.max_total',
IntegerField(
'Maximum number of articles to cite in total',
widget=NumberInput(),
validators=[Optional()]))
s_articleCitationMinChars = Setting('article.citation.min_chars',
IntegerField(
'Minimum number of characters to cite articles by',
widget=NumberInput(),
validators=[Optional()]))
s_articleCitationMaxChars = Setting('article.citation.max_chars',
IntegerField(
'Maximum number of characters to cite articles by',
widget=NumberInput(),
validators=[Optional()]))
s_articleWordLimitSoft = Setting('article.word_limit.soft',
IntegerField(
'Soft word limit',
widget=NumberInput(),
validators=[Optional()]))
s_articleWordLimitHard = Setting('article.word_limit.hard',
IntegerField(
'Hard word limit',
widget=NumberInput(),
validators=[Optional()]))
s_articleAddendumAllowed = Setting('article.addendum.allowed',
BooleanField('Allow addendum articles'))
s_articleAddendumMax = Setting('article.addendum.max',
IntegerField(
'Maximum number of addendum articles per character per turn',
widget=NumberInput(),
validators=[Optional()]))
class ConfigFormBase(FlaskForm):
def __init__(self, lexicon):
super().__init__()
editor_field = getattr(self, 'editor', None)
if editor_field:
model_factory: ModelFactory = current_app.config['model_factory']
editor_field.choices = list(map(
lambda s: (s, s),
map(
lambda uid: model_factory.user(uid).cfg.username,
lexicon.cfg.join.joined)))
def load(self, lexicon):
for k, v in Settings.settings():
field = getattr(self, k[2:], None)
if field:
v.load(lexicon.cfg, field)
def save(self, lexicon):
with lexicon.ctx.edit_config() as cfg:
for k, v in Settings.settings():
field = getattr(self, k[2:], None)
if field:
v.save(cfg, field)
for k, v in Settings.settings():
setattr(ConfigFormBase, k[2:], v.field)