From 0fd744537cfbe8a9c9de6f864c14980323d5b062 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 30 Jan 2020 22:06:22 -0800 Subject: [PATCH] Give settings page a real form-based UI --- amanuensis/lexicon/__init__.py | 3 + amanuensis/resources/page.css | 3 + amanuensis/server/forms.py | 146 ++++++++++++++++++++- amanuensis/server/lexicon.py | 27 +--- amanuensis/templates/lexicon/session.html | 3 + amanuensis/templates/lexicon/settings.html | 101 +++++++++++++- 6 files changed, 257 insertions(+), 26 deletions(-) diff --git a/amanuensis/lexicon/__init__.py b/amanuensis/lexicon/__init__.py index 5bdc579..acf44e5 100644 --- a/amanuensis/lexicon/__init__.py +++ b/amanuensis/lexicon/__init__.py @@ -52,6 +52,9 @@ class LexiconModel(): def __repr__(self): return ''.format(self) + def edit(self): + return json_rw(self.config_path) + def add_log(self, message): now = int(time.time()) with json_rw(self.config_path) as j: diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index 7d0b6ec..38e18a3 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -111,6 +111,9 @@ textarea.fullwidth { width: 100%; box-sizing: border-box; } +input.smallnumber { + width: 4em; +} div.dashboard-lexicon-item { margin: 0 10px; padding: 0 10px; diff --git a/amanuensis/server/forms.py b/amanuensis/server/forms.py index 7c2e0f4..a6684f5 100644 --- a/amanuensis/server/forms.py +++ b/amanuensis/server/forms.py @@ -1,9 +1,12 @@ from flask_wtf import FlaskForm from wtforms import ( - StringField, PasswordField, BooleanField, SubmitField, TextAreaField) -from wtforms.validators import DataRequired, ValidationError + StringField, PasswordField, BooleanField, SubmitField, TextAreaField, + IntegerField, SelectField) +from wtforms.validators import DataRequired, ValidationError, Optional +from wtforms.widgets.html5 import NumberInput from amanuensis.config import json_ro +from amanuensis.user import UserModel # Custom validators @@ -52,9 +55,146 @@ class LexiconCreateForm(FlaskForm): class LexiconConfigForm(FlaskForm): """/lexicon//session/settings/""" - configText = TextAreaField("Config file") + # General + title = StringField('Title override', validators=[Optional()]) + editor = SelectField('Editor', validators=[DataRequired(), user(exists=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", + 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") + # TODO add validators that call into extant valid check methods + + 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))) + + def populate_from_lexicon(self, lexicon): + self.title.data = lexicon.title + self.editor.data = UserModel.by(uid=lexicon.editor).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.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: + 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.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 + class LexiconJoinForm(FlaskForm): """/lexicon//join/""" diff --git a/amanuensis/server/lexicon.py b/amanuensis/server/lexicon.py index 87f17bf..f8b6cad 100644 --- a/amanuensis/server/lexicon.py +++ b/amanuensis/server/lexicon.py @@ -65,36 +65,23 @@ def get_bp(): @lexicon_param @editor_required def settings(name): - # Restrict to editor - if not current_user.id == g.lexicon.editor: - flash("Access is forbidden") - return redirect(url_for('lexicon.session', name=name)) - form = LexiconConfigForm() + form.set_options(g.lexicon) # Load the config for the lexicon on load if not form.is_submitted(): - with json_ro(g.lexicon.config_path) as cfg: - form.configText.data = json.dumps(cfg, indent=2) + form.populate_from_lexicon(g.lexicon) return render_template("lexicon/settings.html", form=form) if form.validate(): - # Check input is valid json - try: - cfg = json.loads(form.configText.data, - object_pairs_hook=ReadOnlyOrderedDict) - except json.decoder.JsonDecodeError: - flash("Invalid JSON") + if not form.update_lexicon(g.lexicon): + flash("Error updating settings") return render_template("lexicon/settings.html", form=form) - # Check input has all the required fields - # TODO - # Write the new config form.submit.submitted = False - with open_ex(g.lexicon.config_path, mode='w') as f: - json.dump(cfg, f, indent='\t') - flash("Config updated") - return redirect(url_for('lexicon.settings', name=name)) + flash("Settings updated") + return redirect(url_for('lexicon.session', name=name)) + flash("Validation error") return render_template("lexicon/settings.html", form=form) @bp.route('/statistics/', methods=['GET']) diff --git a/amanuensis/templates/lexicon/session.html b/amanuensis/templates/lexicon/session.html index 430a906..2fb7494 100644 --- a/amanuensis/templates/lexicon/session.html +++ b/amanuensis/templates/lexicon/session.html @@ -15,6 +15,9 @@ {% endif %} {% block main %} +{% for message in get_flashed_messages() %} +{{ message }}
+{% endfor %}

Placeholder text for session page

{% endblock %} {% set template_content_blocks = template_content_blocks + [self.main()] %} \ No newline at end of file diff --git a/amanuensis/templates/lexicon/settings.html b/amanuensis/templates/lexicon/settings.html index 08002a7..a606957 100644 --- a/amanuensis/templates/lexicon/settings.html +++ b/amanuensis/templates/lexicon/settings.html @@ -1,14 +1,109 @@ {% extends "lexicon/lexicon.html" %} {% block title %}Edit | {{ lexicon_title }}{% endblock %} -{% block main %} +{% block info %} +

+ Id: {{ g.lexicon.id }}
+ Name: {{ g.lexicon.name }}
+ Created: {{ g.lexicon.time.created|asdate }}
+ Completed: {{ g.lexicon.time.completed|asdate }}
+ Players: + {% for uid in g.lexicon.join.joined[:-1] %} + {{ uid|user_attr('username') }}, + {% endfor %} + {{ g.lexicon.join.joined[-1]|user_attr('username') }}
+ Log: +

+ {% for log_entry in g.lexicon.log %} + [{{ log_entry[0]|asdate }}] {{ log_entry[1] }}
+ {% endfor %} +
+

+{% endblock %} + +{% macro number_setting(field) %} +{{ field(autocomplete="off", class_="smallnumber") }} +{{ field.label }}
+{% for error in field.errors %} +{{ error }}
+{% endfor %} + +{% endmacro %} +{% macro flag_setting(field) %} +{{ field() }} +{{ field.label }}
+{% endmacro %} + +{% block settings %}
{{ form.hidden_tag() }} -

{{ form.configText.label }}
{{ form.configText(rows=20, class_="fullwidth") }}

+ +

General

+

+ {{ form.title.label }}:
+ {{ form.title(autocomplete="off", size=32, style="width:100%") }}
+ {{ form.editor.label }}: {{ form.editor(autocomplete="off") }}
+ {% for error in form.editor.errors %} + {{ error }}
+ {% endfor %} + {{ form.prompt.label }}: {{ form.prompt(class_="fullwidth") }} + {% for error in form.prompt.errors %} + {{ error }}
+ {% endfor %} +

+ +

Game Progress

+

+ {{ number_setting(form.turnCurrent) }} + {{ number_setting(form.turnMax) }} + {{ form.articleIndexList.label }}:
+ {{ form.articleIndexList(class_="fullwidth", rows=10) }} + {{ number_setting(form.articleIndexCapacity) }} +

+ +

Visibility and Joining

+

+ {{ flag_setting(form.joinPublic) }} + {{ flag_setting(form.joinOpen) }} + {{ form.joinPassword(autocomplete="off") }} + {{ form.joinPassword.label }}
+ {{ number_setting(form.joinMaxPlayers) }} +

+ +

Turn Publishing

+

+ {{ flag_setting(form.publishNotifyEditorOnReady) }} + {{ flag_setting(form.publishNotifyPlayerOnReject) }} + {{ flag_setting(form.publishNotifyPlayerOnAccept) }} + {{ form.publishDeadlines(autocomplete="off") }} + {{ form.publishDeadlines.label }}
+ {{ flag_setting(form.publishAsap) }} + {{ flag_setting(form.publishBlockOnReady) }} + {{ number_setting(form.publishQuorum) }} +

+ +

Article Requirements

+

+ {{ flag_setting(form.articleCitationAllowSelf) }} + {{ number_setting(form.articleCitationMinExtant)}} + {{ number_setting(form.articleCitationMaxExtant)}} + {{ number_setting(form.articleCitationMinPhantom)}} + {{ number_setting(form.articleCitationMaxPhantom)}} + {{ number_setting(form.articleCitationMinTotal)}} + {{ number_setting(form.articleCitationMaxTotal)}} + {{ number_setting(form.articleCitationMinChars)}} + {{ number_setting(form.articleCitationMaxChars)}} + {{ number_setting(form.articleWordLimitSoft)}} + {{ number_setting(form.articleWordLimitHard)}} + {{ flag_setting(form.articleAddendumAllowed) }} + {{ number_setting(form.articleAddendumMax) }} +

+

{{ form.submit() }}

{% for message in get_flashed_messages() %} {{ message }}
{% endfor %} {% endblock %} -{% set template_content_blocks = [self.main()] %} \ No newline at end of file +{% set template_content_blocks = [self.info(), self.settings()] %} \ No newline at end of file