You have created {{ characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count }} out of {{ g.lexicon.character_limit }} allowed characters.
Player: {{ character.user.username }}
{% if character.user == current_user %} - + {% endif %} {% endfor %} diff --git a/amanuensis/server/lexicon/settings/__init__.py b/amanuensis/server/lexicon/settings/__init__.py new file mode 100644 index 0000000..26b4558 --- /dev/null +++ b/amanuensis/server/lexicon/settings/__init__.py @@ -0,0 +1,195 @@ +from typing import Sequence + +from flask import Blueprint, render_template, url_for, g, flash, redirect + +from amanuensis.backend import * +from amanuensis.db import * +from amanuensis.server.helpers import ( + editor_required, + lexicon_param, + player_required, + current_membership, + current_lexicon, +) + +from .forms import PlayerSettingsForm, SetupSettingsForm, IndexSchemaForm + + +bp = Blueprint("settings", __name__, url_prefix="/settings", template_folder=".") + + +@bp.get("/") +@lexicon_param +@player_required +def page(lexicon_name): + return redirect(url_for("lexicon.settings.player", lexicon_name=lexicon_name)) + + +@bp.route("/player/", methods=["GET", "POST"]) +@lexicon_param +@player_required +def player(lexicon_name): + form = PlayerSettingsForm() + mem: Membership = current_membership + + if not form.is_submitted(): + # GET + form.notify_ready.data = mem.notify_ready + form.notify_reject.data = mem.notify_reject + form.notify_approve.data = mem.notify_approve + return render_template( + "settings.jinja", + lexicon_name=lexicon_name, + page_name=player.__name__, + form=form, + ) + + else: + # POST + if form.validate(): + # Data is valid + mem.notify_ready = form.notify_ready.data + mem.notify_reject = form.notify_reject.data + mem.notify_approve = form.notify_approve.data + g.db.session.commit() # TODO refactor into backend + flash("Settings saved") + return redirect( + url_for("lexicon.settings.player", lexicon_name=lexicon_name) + ) + + else: + # Invalid POST data + return render_template( + "settings.jinja", + lexicon_name=lexicon_name, + page_name=player.__name__, + form=form, + ) + + +@bp.route("/setup/", methods=["GET", "POST"]) +@lexicon_param +@editor_required +def setup(lexicon_name): + form = SetupSettingsForm() + lexicon: Lexicon = current_lexicon + + if not form.is_submitted(): + # GET + form.title.data = lexicon.title + form.prompt.data = lexicon.prompt + form.public.data = lexicon.public + form.joinable.data = lexicon.joinable + form.has_password.data = lexicon.join_password is not None + form.turn_count.data = lexicon.turn_count + form.player_limit.data = lexicon.player_limit + form.character_limit.data = lexicon.character_limit + return render_template( + "settings.jinja", + lexicon_name=lexicon_name, + page_name=setup.__name__, + form=form, + ) + + else: + # POST + if form.validate(): + # Data is valid + lexicon.title = form.title.data + lexicon.prompt = form.prompt.data + lexicon.public = form.public.data + lexicon.joinable = form.joinable.data + new_password = form.password.data if form.has_password.data else None + lexiq.password_set(g.db, lexicon.id, new_password) + lexicon.turn_count = form.turn_count.data + lexicon.player_limit = form.player_limit.data + lexicon.character_limit = form.character_limit.data + g.db.session.commit() # TODO refactor into backend + flash("Settings saved") + return redirect( + url_for("lexicon.settings.setup", lexicon_name=lexicon_name) + ) + + else: + # Invalid POST data + return render_template( + "settings.jinja", + lexicon_name=lexicon_name, + page_name=setup.__name__, + form=form, + ) + + +@bp.get("/index/") +@lexicon_param +@editor_required +def index(lexicon_name): + # Get the current indices + indices: Sequence[ArticleIndex] = indq.get_for_lexicon(g.db, current_lexicon.id) + index_data = [ + { + "index_type": str(index.index_type), + "pattern": index.pattern, + "logical_order": index.logical_order, + "display_order": index.display_order, + "capacity": index.capacity, + } + for index in indices + ] + # Add a blank index to allow for adding rules + index_data.append( + { + "index_type": "", + "pattern": None, + "logical_order": None, + "display_order": None, + "capacity": None, + } + ) + form = IndexSchemaForm(indices=index_data) + return render_template( + "settings.jinja", lexicon_name=lexicon_name, page_name=index.__name__, form=form + ) + + +@bp.post("/index/") +@lexicon_param +@editor_required +def index_post(lexicon_name): + # Initialize the form + form = IndexSchemaForm() + if form.validate(): + # Valid data, strip out all indexes with the blank type + indices = [ + index_def.to_model() + for index_def in form.indices.entries + if index_def.index_type.data + ] + indq.update(g.db, current_lexicon.id, indices) + return redirect(url_for("lexicon.settings.index", lexicon_name=lexicon_name)) + else: + # Invalid data + return render_template( + "settings.jinja", + lexicon_name=lexicon_name, + page_name=index.__name__, + form=form, + ) + + +@bp.get("/publish/") +@lexicon_param +@editor_required +def publish(lexicon_name): + return render_template( + "settings.jinja", lexicon_name=lexicon_name, page_name=publish.__name__ + ) + + +@bp.get("/article/") +@lexicon_param +@editor_required +def article(lexicon_name): + return render_template( + "settings.jinja", lexicon_name=lexicon_name, page_name=article.__name__ + ) diff --git a/amanuensis/server/lexicon/settings/forms.py b/amanuensis/server/lexicon/settings/forms.py new file mode 100644 index 0000000..7b9209c --- /dev/null +++ b/amanuensis/server/lexicon/settings/forms.py @@ -0,0 +1,97 @@ +from flask_wtf import FlaskForm +from wtforms import ( + BooleanField, + FieldList, + FormField, + IntegerField, + PasswordField, + SelectField, + StringField, + SubmitField, + TextAreaField, +) +from wtforms.validators import Optional, DataRequired, ValidationError +from wtforms.widgets.html5 import NumberInput + +from amanuensis.db import ArticleIndex, IndexType + + +class PlayerSettingsForm(FlaskForm): + """/lexicon/These settings are specific to you as a player in this lexicon.
+ + + {% for message in get_flashed_messages() %} + {{ message }}An index is a rule that matches the title of a lexicon article based on its index type and pattern. A char index matches a title if the first letter of the title (excluding "A", "An", and "The") is one of the letters in the pattern. A range index has a pattern denoting a range of letters, such as "A-F", and matches a title if the first letter of the title is in the range. A prefix index matches any title that begins with the pattern. An etc index always matches a title.
+When a title is to be sorted under an index, indices are checked in order, sorted first by descending order of logical priority, and then by alphabetical order of index pattern. The title is sorted under the first index that matches it.
+On the contents page, indices and the articles under them are displayed sorted instead by display order and then alphabetically by pattern.
+The capacity of an index is the number of articles that may exist under that index. If an index is at capacity, no new articles may be written or created via phantom citation in that index.
+To add an index, fill in the type and pattern in the blank row and save your changes. To remove an index, set the type to blank. Note: If you change the type or pattern of an index, all index assignments will be reset. Avoid changing index definitions during gameplay.
+- {{ lexicon.full_title }} + {{ lexicon.full_title }} [{{ status.capitalize() }}]
@@ -29,7 +29,7 @@ Players: {{ lexicon.memberships|count }}{% if lexicon.player_limit is not none %} / {{ lexicon.player_limit }}{% endif -%} {%- if lexicon.public and lexicon.joinable - %} / Join game + %} / Join game {%- endif -%} {%- endif -%} diff --git a/amanuensis/server/session/session.settings.jinja b/amanuensis/server/session/session.settings.jinja index a1395f8..56900c6 100644 --- a/amanuensis/server/session/session.settings.jinja +++ b/amanuensis/server/session/session.settings.jinja @@ -45,47 +45,21 @@
- {{ 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 %}
{{ number_setting(form.turnCurrent) }}
- {{ number_setting(form.turnMax) }}
- {{ form.articleIndexList.label }}:
- {{ form.articleIndexList(class_="fullwidth", rows=10) }}
- {% for error in form.articleIndexList.errors %}
- {{ error }}
- {% endfor %}
- {{ number_setting(form.articleIndexCapacity) }}
{{ form.turnAssignment.label }}:
{{ form.turnAssignment(class_="fullwidth", rows=10) }}
- {{ flag_setting(form.joinPublic) }}
- {{ flag_setting(form.joinOpen) }}
- {{ form.joinPassword(autocomplete="off") }}
- {{ form.joinPassword.label }}
- {{ number_setting(form.joinMaxPlayers) }}
- {{ number_setting(form.joinCharsPerPlayer) }}
-
- {{ flag_setting(form.publishNotifyEditorOnReady) }}
- {{ flag_setting(form.publishNotifyPlayerOnReject) }}
- {{ flag_setting(form.publishNotifyPlayerOnAccept) }}
{{ form.publishDeadlines(autocomplete="off") }}
{{ form.publishDeadlines.label }}
{{ flag_setting(form.publishAsap) }}
diff --git a/amanuensis/server/session/settings.py b/amanuensis/server/session/settings.py
index ce1dd03..e37809e 100644
--- a/amanuensis/server/session/settings.py
+++ b/amanuensis/server/session/settings.py
@@ -158,65 +158,20 @@ class Settings():
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_turnAssignment = Setting('turn.assignment',
TextAreaField('index assignment raw'),
translator=TmpAsgnTranslator())
- 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',
@@ -236,18 +191,6 @@ class Settings():
BooleanField(
'Block turn publish if any articles are awaiting editor review'))
- s_articleIndexList = Setting('article.index.list',
- TextAreaField(
- 'Index specifications',
- validators=[IndexList]),
- translator=IndexListTranslator())
-
- 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'))
diff --git a/tests/test_character.py b/tests/test_character.py
index ccd5b51..d21c3e2 100644
--- a/tests/test_character.py
+++ b/tests/test_character.py
@@ -15,7 +15,6 @@ def test_character_view(db: DbContext, app: Flask, make: ObjectFactory):
username: str = f"user_{os.urandom(8).hex()}"
charname: str = f"char_{os.urandom(8).hex()}"
char_sig: str = f"signature_{os.urandom(8).hex()}"
- # ub: bytes = username.encode("utf8")
with app.test_client() as client:
# Create the user and log in
@@ -32,12 +31,12 @@ def test_character_view(db: DbContext, app: Flask, make: ObjectFactory):
assert mem
# The character page exists
- list_url = url_for("lexicon.characters.list", name=lexicon.name)
+ list_url = url_for("lexicon.characters.list", lexicon_name=lexicon.name)
response = client.get(list_url)
assert response.status_code == 200
assert charname.encode("utf8") not in response.data
assert char_sig.encode("utf8") not in response.data
- new_url = url_for("lexicon.characters.new", name=lexicon.name)
+ new_url = url_for("lexicon.characters.new", lexicon_name=lexicon.name)
assert new_url.encode("utf8") in response.data
# The character creation endpoint works
@@ -63,7 +62,6 @@ def test_character_view(db: DbContext, app: Flask, make: ObjectFactory):
created_redirect,
data={"name": charname, "signature": char_sig, "csrf_token": csrf_token},
)
- print(response.data.decode("utf8"))
assert 300 <= response.status_code <= 399
# The character is updated
diff --git a/tests/test_index.py b/tests/test_index.py
new file mode 100644
index 0000000..10300ff
--- /dev/null
+++ b/tests/test_index.py
@@ -0,0 +1,91 @@
+from amanuensis.db.models import IndexType
+import os
+from urllib.parse import urlsplit
+
+from bs4 import BeautifulSoup
+from flask import Flask, url_for
+
+from amanuensis.backend import memq, charq, indq
+from amanuensis.db import DbContext
+
+from tests.conftest import ObjectFactory
+
+
+def test_index_view(db: DbContext, app: Flask, make: ObjectFactory):
+ """Test the lexicon index page"""
+
+ with app.test_client() as client:
+ # Create the user and log in
+ user = make.user()
+ assert user
+ user_client = make.client(user.id)
+ assert client
+ user_client.login(client)
+
+ # Create a lexicon and join as the editor
+ lexicon = make.lexicon()
+ assert lexicon
+ mem = memq.create(db, user.id, lexicon.id, is_editor=True)
+ assert mem
+
+ # The index settings page exists
+ index_settings = url_for("lexicon.settings.index", lexicon_name=lexicon.name)
+ response = client.get(index_settings)
+ assert response.status_code == 200
+
+ # Add some indices
+ i1 = indq.create(db, lexicon.id, IndexType.CHAR, "ABCDE", 0, 0, 0)
+ assert i1
+ p1 = i1.pattern
+ assert p1
+ i2 = indq.create(db, lexicon.id, IndexType.RANGE, "F-M", 0, 0, 0)
+ assert i2
+ p2 = i2.pattern
+ assert p2
+ i3 = indq.create(db, lexicon.id, IndexType.CHAR, "NOPQ", 0, 0, 0)
+ assert i3
+ p3 = i3.pattern
+ assert p3
+ db.session.commit()
+
+ # The index settings page shows the indices
+ response = client.get(index_settings)
+ assert response.status_code == 200
+ # for i in indq.get_for_lexicon(db, lexicon.id):
+ assert p1.encode("utf8") in response.data
+ assert p2.encode("utf8") in response.data
+ assert p3.encode("utf8") in response.data
+
+ # Indices can be modified
+ soup = BeautifulSoup(response.data, features="html.parser")
+ csrf_token = soup.find(id="csrf_token")["value"]
+ assert csrf_token
+ response = client.post(
+ index_settings,
+ data={
+ "csrf_token": csrf_token,
+ "indices-0-index_type": "CHAR",
+ "indices-0-pattern": "ABCDEF",
+ "indices-0-logical_order": 0,
+ "indices-0-display_order": 0,
+ "indices-0-capacity": "",
+ "indices-1-index_type": "PREFIX",
+ "indices-1-pattern": "F-M",
+ "indices-1-logical_order": 1,
+ "indices-1-display_order": -1,
+ "indices-1-capacity": "",
+ "indices-2-index_type": "",
+ "indices-2-pattern": "NOPQ",
+ "indices-2-logical_order": 0,
+ "indices-2-display_order": 0,
+ "indices-2-capacity": "",
+ },
+ )
+ assert 300 <= response.status_code <= 399
+
+ updated_indices = list(indq.get_for_lexicon(db, lexicon.id))
+ assert len(updated_indices) == 2
+ assert updated_indices[0].index_type == IndexType.CHAR
+ assert updated_indices[0].pattern == "ABCDEF"
+ assert updated_indices[1].index_type == IndexType.PREFIX
+ assert updated_indices[1].pattern == "F-M"