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

View File

@ -3,6 +3,8 @@ from wtforms import (
StringField, SubmitField, TextAreaField, RadioField) StringField, SubmitField, TextAreaField, RadioField)
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
from .settings import ConfigFormBase
class LexiconCharacterForm(FlaskForm): class LexiconCharacterForm(FlaskForm):
"""/lexicon/<name>/session/character/""" """/lexicon/<name>/session/character/"""
@ -36,3 +38,8 @@ class LexiconReviewForm(FlaskForm):
class LexiconPublishTurnForm(FlaskForm): class LexiconPublishTurnForm(FlaskForm):
"""/lexicon/<name>/session/""" """/lexicon/<name>/session/"""
submit = SubmitField('Publish turn') 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 flask_wtf import FlaskForm
from wtforms import ( from wtforms import (
StringField, BooleanField, SubmitField, TextAreaField, Field,
IntegerField, SelectField) StringField,
from wtforms.validators import DataRequired, ValidationError, Optional BooleanField,
TextAreaField,
IntegerField,
SelectField)
from wtforms.validators import DataRequired, Optional
from wtforms.widgets.html5 import NumberInput from wtforms.widgets.html5 import NumberInput
from amanuensis.config import ReadOnlyOrderedDict, AttrOrderedDict
from amanuensis.models import ModelFactory, UserModel
from amanuensis.server.forms import User from amanuensis.server.forms import User
class LexiconConfigForm(FlaskForm): class SettingTranslator():
"""/lexicon/<name>/session/settings/""" """
# General Base class for the translation layer between internal config data
title = StringField('Title override', validators=[Optional()]) and user-friendly display in the settings form. By default the data
editor = SelectField('Editor', validators=[DataRequired(), User(True)]) is returned as-is.
prompt = TextAreaField('Prompt', validators=[DataRequired()]) """
# Turn def load(self, cfg_value):
turnCurrent = IntegerField('Current turn', widget=NumberInput(), validators=[Optional()]) return cfg_value
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",
widget=NumberInput(),
validators=[DataRequired()])
joinCharsPerPlayer = IntegerField(
"Characters per player",
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")
def validate_publishDeadlines(form, field): def save(self, field_data):
if form.publishAsap.data: return field_data
raise ValidationError('Cannot specify deadline if immediate publishing is enabled')
# TODO add validators that call into extant valid check methods
# def set_options(self, lexicon): class UsernameTranslator(SettingTranslator):
# self.editor.choices = list(map(lambda x: (x, x), map( """
# lambda uid: UserModel.by(uid=uid).username, Converts an internal user id to a public-facing username.
# lexicon.join.joined))) """
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 populate_from_lexicon(self, lexicon): def save(self, field_data):
# self.title.data = lexicon.cfg.title model_factory: ModelFactory = current_app.config['model_factory']
# self.editor.data = ModelFactory(lexicon.ctx.root).user(lexicon.cfg.editor).cfg.username user: UserModel = model_factory.try_user(field_data)
# self.prompt.data = lexicon.prompt if user:
# self.turnCurrent.data = lexicon.turn.current return user.uid
# 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
# def update_lexicon(self, lexicon):
# with lexicon.edit() as l: class Setting():
# l.title = self.title.data """
# l.editor = UserModel.by(name=self.editor.data).uid Represents a relation between a node in a lexicon config and a
# l.prompt = self.prompt.data field in a public-facing form that exposes it to the editor for
# l.turn.current = self.turnCurrent.data modification.
# l.turn.max = self.turnMax.data """
# l.join.public = self.joinPublic.data def __init__(
# l.join.open = self.joinOpen.data self,
# l.join.password = self.joinPassword.data cfg_key: str,
# l.join.max_players = self.joinMaxPlayers.data field: Field,
# l.join.chars_per_player = self.joinCharsPerPlayer.data translator: SettingTranslator = SettingTranslator()):
# l.publish.notify.editor_on_ready = self.publishNotifyEditorOnReady.data """
# l.publish.notify.player_on_reject = self.publishNotifyPlayerOnReject.data Creates a setting. Optionally, defines a nontrivial translation
# l.publish.notify.player_on_accept = self.publishNotifyPlayerOnAccept.data between internal and public values.
# l.publish.deadlines = self.publishDeadlines.data """
# l.publish.asap = self.publishAsap.data self.cfg_path = cfg_key.split('.')
# l.publish.quorum = self.publishQuorum.data self.field = field
# l.publish.block_on_ready = self.publishBlockOnReady.data self.translator = translator
# l.article.index.list = self.articleIndexList.data
# l.article.index.capacity = self.articleIndexCapacity.data def load(self, cfg: ReadOnlyOrderedDict, field: Field):
# l.article.citation.allow_self = self.articleCitationAllowSelf.data """
# l.article.citation.min_extant = self.articleCitationMinExtant.data Sets the field's value to the corresponding config node
# l.article.citation.max_extant = self.articleCitationMaxExtant.data """
# l.article.citation.min_phantom = self.articleCitationMinPhantom.data for key in self.cfg_path[:-1]:
# l.article.citation.max_phantom = self.articleCitationMaxPhantom.data cfg = cast(ReadOnlyOrderedDict, cfg.get(key))
# l.article.citation.min_total = self.articleCitationMinTotal.data data = cfg.get(self.cfg_path[-1])
# l.article.citation.max_total = self.articleCitationMaxTotal.data field.data = self.translator.load(data)
# l.article.citation.min_chars = self.articleCitationMinChars.data
# l.article.citation.max_chars = self.articleCitationMaxChars.data def save(self, cfg: AttrOrderedDict, field: Field):
# l.article.word_limit.soft = self.articleWordLimitSoft.data """
# l.article.word_limit.hard = self.articleWordLimitHard.data Updates the editable config with this field's value
# l.article.addendum.allowed = self.articleAddendumAllowed.data """
# l.article.addendum.max = self.articleAddendumMax.data for key in self.cfg_path[:-1]:
# return True 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=[Optional()]))
s_turnMax = Setting('turn.max',
IntegerField(
'Number of turns',
widget=NumberInput(),
validators=[DataRequired()]))
s_joinPublic = Setting('join.public',
BooleanField('Show game on public pages'))
s_joinOpen = Setting('join.open',
BooleanField('Allow players to join game'))
s_joinPassword = Setting('join.password',
StringField('Password to join game', validators=[Optional()]))
s_joinMaxPlayers = Setting('join.max_players',
IntegerField(
'Maximum number of players',
widget=NumberInput(),
validators=[DataRequired()]))
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)