Give settings page a real form-based UI
This commit is contained in:
parent
3c2458ae58
commit
0fd744537c
|
@ -52,6 +52,9 @@ class LexiconModel():
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<LexiconModel lid={0.id} name={0.name}>'.format(self)
|
return '<LexiconModel lid={0.id} name={0.name}>'.format(self)
|
||||||
|
|
||||||
|
def edit(self):
|
||||||
|
return json_rw(self.config_path)
|
||||||
|
|
||||||
def add_log(self, message):
|
def add_log(self, message):
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
with json_rw(self.config_path) as j:
|
with json_rw(self.config_path) as j:
|
||||||
|
|
|
@ -111,6 +111,9 @@ textarea.fullwidth {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
input.smallnumber {
|
||||||
|
width: 4em;
|
||||||
|
}
|
||||||
div.dashboard-lexicon-item {
|
div.dashboard-lexicon-item {
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
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,
|
||||||
from wtforms.validators import DataRequired, ValidationError
|
IntegerField, SelectField)
|
||||||
|
from wtforms.validators import DataRequired, ValidationError, Optional
|
||||||
|
from wtforms.widgets.html5 import NumberInput
|
||||||
|
|
||||||
from amanuensis.config import json_ro
|
from amanuensis.config import json_ro
|
||||||
|
from amanuensis.user import UserModel
|
||||||
|
|
||||||
|
|
||||||
# Custom validators
|
# Custom validators
|
||||||
|
@ -52,9 +55,146 @@ class LexiconCreateForm(FlaskForm):
|
||||||
|
|
||||||
class LexiconConfigForm(FlaskForm):
|
class LexiconConfigForm(FlaskForm):
|
||||||
"""/lexicon/<name>/session/settings/"""
|
"""/lexicon/<name>/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")
|
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):
|
class LexiconJoinForm(FlaskForm):
|
||||||
"""/lexicon/<name>/join/"""
|
"""/lexicon/<name>/join/"""
|
||||||
|
|
|
@ -65,36 +65,23 @@ def get_bp():
|
||||||
@lexicon_param
|
@lexicon_param
|
||||||
@editor_required
|
@editor_required
|
||||||
def settings(name):
|
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 = LexiconConfigForm()
|
||||||
|
form.set_options(g.lexicon)
|
||||||
|
|
||||||
# Load the config for the lexicon on load
|
# Load the config for the lexicon on load
|
||||||
if not form.is_submitted():
|
if not form.is_submitted():
|
||||||
with json_ro(g.lexicon.config_path) as cfg:
|
form.populate_from_lexicon(g.lexicon)
|
||||||
form.configText.data = json.dumps(cfg, indent=2)
|
|
||||||
return render_template("lexicon/settings.html", form=form)
|
return render_template("lexicon/settings.html", form=form)
|
||||||
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
# Check input is valid json
|
if not form.update_lexicon(g.lexicon):
|
||||||
try:
|
flash("Error updating settings")
|
||||||
cfg = json.loads(form.configText.data,
|
|
||||||
object_pairs_hook=ReadOnlyOrderedDict)
|
|
||||||
except json.decoder.JsonDecodeError:
|
|
||||||
flash("Invalid JSON")
|
|
||||||
return render_template("lexicon/settings.html", form=form)
|
return render_template("lexicon/settings.html", form=form)
|
||||||
# Check input has all the required fields
|
|
||||||
# TODO
|
|
||||||
# Write the new config
|
|
||||||
form.submit.submitted = False
|
form.submit.submitted = False
|
||||||
with open_ex(g.lexicon.config_path, mode='w') as f:
|
flash("Settings updated")
|
||||||
json.dump(cfg, f, indent='\t')
|
return redirect(url_for('lexicon.session', name=name))
|
||||||
flash("Config updated")
|
|
||||||
return redirect(url_for('lexicon.settings', name=name))
|
|
||||||
|
|
||||||
|
flash("Validation error")
|
||||||
return render_template("lexicon/settings.html", form=form)
|
return render_template("lexicon/settings.html", form=form)
|
||||||
|
|
||||||
@bp.route('/statistics/', methods=['GET'])
|
@bp.route('/statistics/', methods=['GET'])
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
{% for message in get_flashed_messages() %}
|
||||||
|
<span style="color: #ff0000">{{ message }}</span><br>
|
||||||
|
{% endfor %}
|
||||||
<p>Placeholder text for session page</p>
|
<p>Placeholder text for session page</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% set template_content_blocks = template_content_blocks + [self.main()] %}
|
{% set template_content_blocks = template_content_blocks + [self.main()] %}
|
|
@ -1,14 +1,109 @@
|
||||||
{% extends "lexicon/lexicon.html" %}
|
{% extends "lexicon/lexicon.html" %}
|
||||||
{% block title %}Edit | {{ lexicon_title }}{% endblock %}
|
{% block title %}Edit | {{ lexicon_title }}{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block info %}
|
||||||
|
<p>
|
||||||
|
Id: {{ g.lexicon.id }}<br>
|
||||||
|
Name: {{ g.lexicon.name }}<br>
|
||||||
|
Created: {{ g.lexicon.time.created|asdate }}<br>
|
||||||
|
Completed: {{ g.lexicon.time.completed|asdate }}<br>
|
||||||
|
Players:
|
||||||
|
{% for uid in g.lexicon.join.joined[:-1] %}
|
||||||
|
{{ uid|user_attr('username') }},
|
||||||
|
{% endfor %}
|
||||||
|
{{ g.lexicon.join.joined[-1]|user_attr('username') }}<br>
|
||||||
|
Log:
|
||||||
|
<div style="width: 100%; height: 10em; overflow-y:auto; resize: vertical;
|
||||||
|
border: 1px solid #bbbbbb; font-size: 0.9em; padding:3px; box-sizing: border-box;">
|
||||||
|
{% for log_entry in g.lexicon.log %}
|
||||||
|
[{{ log_entry[0]|asdate }}] {{ log_entry[1] }}<br>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% macro number_setting(field) %}
|
||||||
|
{{ field(autocomplete="off", class_="smallnumber") }}
|
||||||
|
{{ field.label }}<br>
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<span style="color: #ff0000">{{ error }}</span><br>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endmacro %}
|
||||||
|
{% macro flag_setting(field) %}
|
||||||
|
{{ field() }}
|
||||||
|
{{ field.label }}<br>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% block settings %}
|
||||||
<form action="" method="post" novalidate>
|
<form action="" method="post" novalidate>
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<p>{{ form.configText.label }}<br>{{ form.configText(rows=20, class_="fullwidth") }}</p>
|
|
||||||
|
<h3>General</h3>
|
||||||
|
<p>
|
||||||
|
{{ form.title.label }}:<br>
|
||||||
|
{{ form.title(autocomplete="off", size=32, style="width:100%") }}<br>
|
||||||
|
{{ form.editor.label }}: {{ form.editor(autocomplete="off") }}<br>
|
||||||
|
{% for error in form.editor.errors %}
|
||||||
|
<span style="color: #ff0000">{{ error }}</span><br>
|
||||||
|
{% endfor %}
|
||||||
|
{{ form.prompt.label }}: {{ form.prompt(class_="fullwidth") }}
|
||||||
|
{% for error in form.prompt.errors %}
|
||||||
|
<span style="color: #ff0000">{{ error }}</span><br>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Game Progress</h3>
|
||||||
|
<p>
|
||||||
|
{{ number_setting(form.turnCurrent) }}
|
||||||
|
{{ number_setting(form.turnMax) }}
|
||||||
|
{{ form.articleIndexList.label }}:<br>
|
||||||
|
{{ form.articleIndexList(class_="fullwidth", rows=10) }}
|
||||||
|
{{ number_setting(form.articleIndexCapacity) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Visibility and Joining</h3>
|
||||||
|
<p>
|
||||||
|
{{ flag_setting(form.joinPublic) }}
|
||||||
|
{{ flag_setting(form.joinOpen) }}
|
||||||
|
{{ form.joinPassword(autocomplete="off") }}
|
||||||
|
{{ form.joinPassword.label }}<br>
|
||||||
|
{{ number_setting(form.joinMaxPlayers) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Turn Publishing</h3>
|
||||||
|
<p>
|
||||||
|
{{ flag_setting(form.publishNotifyEditorOnReady) }}
|
||||||
|
{{ flag_setting(form.publishNotifyPlayerOnReject) }}
|
||||||
|
{{ flag_setting(form.publishNotifyPlayerOnAccept) }}
|
||||||
|
{{ form.publishDeadlines(autocomplete="off") }}
|
||||||
|
{{ form.publishDeadlines.label }}<br>
|
||||||
|
{{ flag_setting(form.publishAsap) }}
|
||||||
|
{{ flag_setting(form.publishBlockOnReady) }}
|
||||||
|
{{ number_setting(form.publishQuorum) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Article Requirements</h3>
|
||||||
|
<p>
|
||||||
|
{{ 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) }}
|
||||||
|
</p>
|
||||||
|
<!--character-->
|
||||||
<p>{{ form.submit() }}</p>
|
<p>{{ form.submit() }}</p>
|
||||||
</form>
|
</form>
|
||||||
{% for message in get_flashed_messages() %}
|
{% for message in get_flashed_messages() %}
|
||||||
<span style="color: #ff0000">{{ message }}</span><br>
|
<span style="color: #ff0000">{{ message }}</span><br>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% set template_content_blocks = [self.main()] %}
|
{% set template_content_blocks = [self.info(), self.settings()] %}
|
Loading…
Reference in New Issue