From ccb285cbf0c4aac8204ceed408ca91066349c7cb Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 4 Sep 2021 09:21:05 -0700 Subject: [PATCH 1/8] Rename route parameter to lexicon_name This disambiguates and future-proofs the route parameters so they can be more easily referenced in before_request contexts shared by all routes. --- amanuensis/server/__init__.py | 2 +- amanuensis/server/helpers.py | 6 ++-- amanuensis/server/lexicon.jinja | 10 +++--- amanuensis/server/lexicon/__init__.py | 32 +++++++++++-------- .../server/lexicon/characters/__init__.py | 32 +++++++++++++------ .../lexicon/characters/characters.jinja | 4 +-- amanuensis/server/macros.jinja | 4 +-- tests/test_character.py | 4 +-- 8 files changed, 56 insertions(+), 38 deletions(-) diff --git a/amanuensis/server/__init__.py b/amanuensis/server/__init__.py index f0f4d2f..31f0e50 100644 --- a/amanuensis/server/__init__.py +++ b/amanuensis/server/__init__.py @@ -27,7 +27,7 @@ def article_link(title): """Get the url for a lexicon by its title""" return url_for( 'lexicon.article', - name=g.lexicon.name, + lexicon_name=g.lexicon.name, title=filesafe_title(title)) diff --git a/amanuensis/server/helpers.py b/amanuensis/server/helpers.py index 06dc06c..8328ade 100644 --- a/amanuensis/server/helpers.py +++ b/amanuensis/server/helpers.py @@ -34,7 +34,7 @@ def lexicon_param(route): @wraps(route) def with_lexicon(*args, **kwargs): db: DbContext = g.db - name: str = kwargs.get('name') + name: str = kwargs.get('lexicon_name') lexicon: Optional[Lexicon] = lexiq.try_from_name(db, name) if lexicon is None: flash(f"Couldn't find a lexicon with the name \"{name}\"") @@ -71,7 +71,7 @@ def player_required(route): if not mem: flash("You must be a player to view this page") if lexicon.public: - return redirect(url_for('lexicon.contents', name=lexicon.name)) + return redirect(url_for('lexicon.contents', lexicon_name=lexicon.name)) else: return redirect(url_for('home.home')) return route(*args, **kwargs) @@ -108,6 +108,6 @@ def editor_required(route): mem: Optional[Membership] = memq.try_from_ids(db, user.id, lexicon.id) if not mem or not mem.is_editor: flash("You must be the editor to view this page") - return redirect(url_for('lexicon.contents', name=lexicon.name)) + return redirect(url_for('lexicon.contents', lexicon_name=lexicon.name)) return route(*args, **kwargs) return editor_route diff --git a/amanuensis/server/lexicon.jinja b/amanuensis/server/lexicon.jinja index 70862b4..1d3e6c2 100644 --- a/amanuensis/server/lexicon.jinja +++ b/amanuensis/server/lexicon.jinja @@ -9,23 +9,23 @@ {% block sb_logo %}{% endblock %} {% block sb_characters %}Characters{% endblock %} {% block sb_contents %}Contents{% endblock %} {% block sb_rules %}Rules{% endblock %} {% block sb_session %}Session{% endblock %} {% block sb_stats %}Statistics{% endblock %} {% if current_user.is_authenticated and ( diff --git a/amanuensis/server/lexicon/__init__.py b/amanuensis/server/lexicon/__init__.py index 0d5abc6..b2fc7a2 100644 --- a/amanuensis/server/lexicon/__init__.py +++ b/amanuensis/server/lexicon/__init__.py @@ -10,14 +10,16 @@ from .characters import bp as characters_bp from .forms import LexiconJoinForm -bp = Blueprint("lexicon", __name__, url_prefix="/lexicon/", template_folder=".") +bp = Blueprint( + "lexicon", __name__, url_prefix="/lexicon/", template_folder="." +) bp.register_blueprint(characters_bp) @bp.route("/join/", methods=["GET", "POST"]) @lexicon_param @login_required -def join(name): +def join(lexicon_name): lexicon: Lexicon = g.lexicon if not lexicon.joinable: flash("This game isn't open for joining") @@ -27,7 +29,9 @@ def join(name): if not form.validate_on_submit(): # GET or POST with invalid form data - return render_template("lexicon.join.jinja", form=form) + return render_template( + "lexicon.join.jinja", lexicon_name=lexicon_name, form=form + ) # POST with valid data # If the game is passworded, check password @@ -37,48 +41,48 @@ def join(name): ): # Bad creds, try again flash("Incorrect password") - return redirect(url_for("lexicon.join", name=name)) + return redirect(url_for("lexicon.join", lexicon_name=lexicon_name)) # If the password was correct, check if the user can join user: User = current_user try: memq.create(db, user.id, lexicon.id, is_editor=False) - return redirect(url_for("session.session", name=name)) + return redirect(url_for("session.session", lexicon_name=lexicon_name)) except ArgumentError: flash("Could not join game") - return redirect(url_for("home.home", name=name)) + return redirect(url_for("home.home", lexicon_name=lexicon_name)) @bp.get("/contents/") @lexicon_param @player_required_if_not_public -def contents(name): +def contents(lexicon_name): # indexed = sort_by_index_spec(info, g.lexicon.cfg.article.index.list) # for articles in indexed.values(): # for i in range(len(articles)): # articles[i] = { # 'title': articles[i], # **info.get(articles[i])} - return render_template("lexicon.contents.jinja") + return render_template("lexicon.contents.jinja", lexicon_name=lexicon_name) @bp.get("/article/") @lexicon_param @player_required_if_not_public -def article(name, title): +def article(lexicon_name, title): # article = {**a, 'html': Markup(a['html'])} - return render_template("lexicon.article.jinja") + return render_template("lexicon.article.jinja", lexicon_name=lexicon_name) @bp.get("/rules/") @lexicon_param @player_required_if_not_public -def rules(name): - return render_template("lexicon.rules.jinja") +def rules(lexicon_name): + return render_template("lexicon.rules.jinja", lexicon_name=lexicon_name) @bp.get("/statistics/") @lexicon_param @player_required_if_not_public -def stats(name): - return render_template("lexicon.statistics.jinja") +def stats(lexicon_name): + return render_template("lexicon.statistics.jinja", lexicon_name=lexicon_name) diff --git a/amanuensis/server/lexicon/characters/__init__.py b/amanuensis/server/lexicon/characters/__init__.py index 000aa68..0a3483c 100644 --- a/amanuensis/server/lexicon/characters/__init__.py +++ b/amanuensis/server/lexicon/characters/__init__.py @@ -18,18 +18,18 @@ bp = Blueprint("characters", __name__, url_prefix="/characters", template_folder @bp.get("/") @lexicon_param @player_required -def list(name): - return render_template("characters.jinja", name=name) +def list(lexicon_name): + return render_template("characters.jinja", lexicon_name=lexicon_name) @bp.route("/edit/<uuid:character_id>", methods=["GET", "POST"]) @lexicon_param @player_required -def edit(name, character_id: uuid.UUID): +def edit(lexicon_name, character_id: uuid.UUID): character: Optional[Character] = charq.try_from_public_id(g.db, character_id) if not character: flash("Character not found") - return redirect(url_for("lexicon.characters.list", name=name)) + return redirect(url_for("lexicon.characters.list", lexicon_name=lexicon_name)) form = CharacterCreateForm() @@ -37,7 +37,12 @@ def edit(name, character_id: uuid.UUID): # GET form.name.data = character.name form.signature.data = character.signature - return render_template("characters.edit.jinja", character=character, form=form) + return render_template( + "characters.edit.jinja", + lexicon_name=lexicon_name, + character=character, + form=form, + ) else: # POST @@ -46,24 +51,33 @@ def edit(name, character_id: uuid.UUID): character.name = form.name.data character.signature = form.signature.data g.db.session.commit() - return redirect(url_for("lexicon.characters.list", name=name)) + return redirect( + url_for("lexicon.characters.list", lexicon_name=lexicon_name) + ) else: # POST submitted invalid data return render_template( - "characters.edit.jinja", character=character, form=form + "characters.edit.jinja", + lexicon_name=lexicon_name, + character=character, + form=form, ) @bp.get("/new/") @lexicon_param @player_required -def new(name): +def new(lexicon_name): dummy_name = f"{current_user.username}'s new character" dummy_signature = "~" char = charq.create( g.db, g.lexicon.id, current_user.id, dummy_name, dummy_signature ) return redirect( - url_for("lexicon.characters.edit", name=name, character_id=char.public_id) + url_for( + "lexicon.characters.edit", + lexicon_name=lexicon_name, + character_id=char.public_id, + ) ) diff --git a/amanuensis/server/lexicon/characters/characters.jinja b/amanuensis/server/lexicon/characters/characters.jinja index 2d0a31c..278324e 100644 --- a/amanuensis/server/lexicon/characters/characters.jinja +++ b/amanuensis/server/lexicon/characters/characters.jinja @@ -13,7 +13,7 @@ <ul class="blockitem-list"> {% if characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count < g.lexicon.character_limit %} <li> -<h3><a href="{{ url_for('lexicon.characters.new', name=name) }}">Create a new character</a></h3> +<h3><a href="{{ url_for('lexicon.characters.new', lexicon_name=lexicon_name) }}">Create a new character</a></h3> <p>You have created {{ characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count }} out of {{ g.lexicon.character_limit }} allowed characters.</p> </li> {% endif %} @@ -25,7 +25,7 @@ {% endif %} <p>Player: {{ character.user.username }}</p> {% if character.user == current_user %} -<p><a href="{{ url_for('lexicon.characters.edit', name=g.lexicon.name, character_id=character.public_id) }}">Edit this character</a></p> +<p><a href="{{ url_for('lexicon.characters.edit', lexicon_name=lexicon_name, character_id=character.public_id) }}">Edit this character</a></p> {% endif %} </li> {% endfor %} diff --git a/amanuensis/server/macros.jinja b/amanuensis/server/macros.jinja index 7d06006..f361c16 100644 --- a/amanuensis/server/macros.jinja +++ b/amanuensis/server/macros.jinja @@ -3,7 +3,7 @@ <div class="dashboard-lexicon-item dashboard-lexicon-{{ status }}"> <p> <span class="dashboard-lexicon-item-title"> - <a href="{{ url_for('lexicon.contents', name=lexicon.name) }}">{{ lexicon.full_title }}</a> + <a href="{{ url_for('lexicon.contents', lexicon_name=lexicon.name) }}">{{ lexicon.full_title }}</a> </span> [{{ status.capitalize() }}] </p> @@ -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 - %} / <a href="{{ url_for('lexicon.join', name=lexicon.name) }}">Join game</a> + %} / <a href="{{ url_for('lexicon.join', lexicon_name=lexicon.name) }}">Join game</a> {%- endif -%} {%- endif -%} </p> diff --git a/tests/test_character.py b/tests/test_character.py index ccd5b51..176bbc5 100644 --- a/tests/test_character.py +++ b/tests/test_character.py @@ -32,12 +32,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 -- 2.44.1 From d05bc3e6899894f52ebc9957e93c0a384ba6bcbc Mon Sep 17 00:00:00 2001 From: Tim Van Baak <tim.vanbaak@gmail.com> Date: Sat, 4 Sep 2021 09:56:15 -0700 Subject: [PATCH 2/8] Add local proxies for the current lexicon and membership This somewhat duplicates loading the objects into flask.g, but doesn't require the lexicon route decorators. Additionally, lazy-loading these objects can save the occasional db call, and for the current membership this is more convenient than having to add a second decorator or staple it to the lexicon loading. --- amanuensis/server/__init__.py | 16 ++++++++--- amanuensis/server/helpers.py | 50 ++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/amanuensis/server/__init__.py b/amanuensis/server/__init__.py index 31f0e50..bb0e0b4 100644 --- a/amanuensis/server/__init__.py +++ b/amanuensis/server/__init__.py @@ -9,7 +9,7 @@ from amanuensis.config import AmanuensisConfig, CommandLineConfig from amanuensis.db import DbContext from amanuensis.parser import filesafe_title import amanuensis.server.auth as auth -from amanuensis.server.helpers import UuidConverter +from amanuensis.server.helpers import UuidConverter, current_lexicon, current_membership import amanuensis.server.home as home import amanuensis.server.lexicon as lexicon @@ -68,13 +68,21 @@ def get_app( app.teardown_appcontext(db_teardown) # Configure jinja options - def include_backend(): - return {"db": db, "lexiq": lexiq, "userq": userq, "memq": memq, "charq": charq} + def add_jinja_context(): + return { + "db": db, + "lexiq": lexiq, + "userq": userq, + "memq": memq, + "charq": charq, + "current_lexicon": current_lexicon, + "current_membership": current_membership + } app.jinja_options.update(trim_blocks=True, lstrip_blocks=True) app.template_filter("date")(date_format) app.template_filter("articlelink")(article_link) - app.context_processor(include_backend) + app.context_processor(add_jinja_context) # Set up uuid route converter app.url_map.converters["uuid"] = UuidConverter diff --git a/amanuensis/server/helpers.py b/amanuensis/server/helpers.py index 8328ade..b622a00 100644 --- a/amanuensis/server/helpers.py +++ b/amanuensis/server/helpers.py @@ -2,8 +2,17 @@ from functools import wraps from typing import Optional, Any from uuid import UUID -from flask import g, flash, redirect, url_for +from flask import ( + _request_ctx_stack, + flash, + g, + has_request_context, + redirect, + request, + url_for, +) from flask_login import current_user +from werkzeug.local import LocalProxy from werkzeug.routing import BaseConverter, ValidationError from amanuensis.backend import lexiq, memq @@ -26,6 +35,45 @@ class UuidConverter(BaseConverter): return str(value) +def get_current_lexicon(): + # Check if the request context is for a lexicon page + if not has_request_context(): + return None + lexicon_name = request.view_args.get("lexicon_name") + if not lexicon_name: + return None + # Pull up the lexicon if it exists and cache it in the request context + if not hasattr(_request_ctx_stack.top, "lexicon"): + db: DbContext = g.db + lexicon: Optional[Lexicon] = lexiq.try_from_name(db, lexicon_name) + setattr(_request_ctx_stack.top, "lexicon", lexicon) + # Return the cached lexicon + return getattr(_request_ctx_stack.top, "lexicon", None) + + +current_lexicon = LocalProxy(get_current_lexicon) + + +def get_current_membership(): + # Base the current membership on the current user and the current lexicon + user: User = current_user + if not user or not user.is_authenticated: + return None + lexicon: Lexicon = current_lexicon + if not lexicon: + return None + # Pull up the membership and cache it in the request context + if not hasattr(_request_ctx_stack.top, "membership"): + db: DbContext = g.db + mem: Membership = memq.try_from_ids(db, user.id, lexicon.id) + setattr(_request_ctx_stack.top, "membership", mem) + # Return cached membership + return getattr(_request_ctx_stack.top, "membership", None) + + +current_membership = LocalProxy(get_current_membership) + + def lexicon_param(route): """ Wrapper for loading a route's lexicon to `g`. -- 2.44.1 From 00bb2aedec57f7d7b9516ec296722b66802c3470 Mon Sep 17 00:00:00 2001 From: Tim Van Baak <tim.vanbaak@gmail.com> Date: Sat, 4 Sep 2021 13:24:11 -0700 Subject: [PATCH 3/8] Refactor some css colors These will be reused in the settings tabs and perhaps elsewhere --- amanuensis/resources/page.css | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index 536b854..b2e3c6f 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -1,3 +1,8 @@ +:root { + --button-default: #dddddd; + --button-hover: #cccccc; + --button-current: #bbbbbb; +} body { background-color: #eeeeee; line-height: 1.4; @@ -57,7 +62,7 @@ div.misclinks table td a { table a { display: flex; padding: 3px; - background-color: #dddddd; + background-color: var(--button-default); border-radius: 5px; text-decoration: none; } @@ -65,13 +70,13 @@ div#sidebar table a { justify-content: center; } table a:hover { - background-color: #cccccc; + background-color: var(--button-hover); } div#sidebar table a.current-page { - background-color: #bbbbbb; + background-color: var(--button-current); } div#sidebar table a.current-page:hover { - background-color: #bbbbbb; + background-color: var(--button-current); } div#sidebar table td { padding: 0px; margin: 3px 0; -- 2.44.1 From bc6f29713e5fbd3bea822a5536d5eab19b3270b5 Mon Sep 17 00:00:00 2001 From: Tim Van Baak <tim.vanbaak@gmail.com> Date: Sat, 4 Sep 2021 13:34:24 -0700 Subject: [PATCH 4/8] Add settings page skeleton --- amanuensis/resources/page.css | 30 +++++++++ amanuensis/server/lexicon.jinja | 23 ++----- amanuensis/server/lexicon/__init__.py | 2 + .../server/lexicon/settings/__init__.py | 65 +++++++++++++++++++ .../server/lexicon/settings/settings.jinja | 45 +++++++++++++ 5 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 amanuensis/server/lexicon/settings/__init__.py create mode 100644 amanuensis/server/lexicon/settings/settings.jinja diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index b2e3c6f..db95940 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -173,6 +173,36 @@ div.dashboard-lexicon-item p { margin-block-start: 0.5em; margin-block-end: 0.5em; } +ul.unordered-tabs { + list-style: none; + margin-block-start: 0; + margin-block-end: 0; + margin-inline-start: 0; + margin-inline-end: 0; + padding-block-start: 0; + padding-block-end: 0; + padding-inline-start: 0; + padding-inline-end: 0; +} +ul.unordered-tabs li { + display: inline-block; + margin: 3px; +} +ul.unordered-tabs li a { + background-color: var(--button-current); + display: flex; + border: 5px solid var(--button-current); + border-radius: 5px; + text-decoration: none; +} +ul.unordered-tabs li a[href] { + background-color: var(--button-default); + border-color: var(--button-default); +} +ul.unordered-tabs li a[href]:hover { + background-color: var(--button-hover); + border-color: var(--button-hover); +} @media only screen and (max-width: 816px) { div#wrapper { padding: 5px; diff --git a/amanuensis/server/lexicon.jinja b/amanuensis/server/lexicon.jinja index 1d3e6c2..46f18c4 100644 --- a/amanuensis/server/lexicon.jinja +++ b/amanuensis/server/lexicon.jinja @@ -19,31 +19,18 @@ {% if current_page == "rules" %}class="current-page" {% else %}href="{{ url_for('lexicon.rules', lexicon_name=g.lexicon.name) }}" {% endif %}>Rules</a>{% endblock %} -{% block sb_session %}<a - {% if current_page == "session" %}class="current-page" - {% else %}href="#{#{ url_for('session.session', lexicon_name=g.lexicon.name) }}" - {% endif %}>Session</a>{% endblock %} +{% block sb_settings %}<a + {% if current_page == "settings" %}class="current-page" + {% else %}href="{{ url_for('lexicon.settings.page', lexicon_name=g.lexicon.name) }}" + {% endif %}>Settings</a>{% endblock %} {% block sb_stats %}<a {% if current_page == "statistics" %}class="current-page" {% else %}href="{{ url_for('lexicon.stats', lexicon_name=g.lexicon.name) }}" {% endif %}>Statistics</a>{% endblock %} -{% if current_user.is_authenticated and ( - current_user.is_site_admin - or memq.try_from_ids(g.db, current_user.id, g.lexicon.id) - ) %} - {# self.sb_logo(), #} {% set template_sidebar_rows = [ self.sb_characters(), self.sb_contents(), self.sb_rules(), - self.sb_session(), + self.sb_settings(), self.sb_stats()] %} -{% else %} - {# self.sb_logo(), #} -{% set template_sidebar_rows = [ - self.sb_characters(), - self.sb_contents(), - self.sb_rules(), - self.sb_stats()] %} -{% endif %} diff --git a/amanuensis/server/lexicon/__init__.py b/amanuensis/server/lexicon/__init__.py index b2fc7a2..a75049e 100644 --- a/amanuensis/server/lexicon/__init__.py +++ b/amanuensis/server/lexicon/__init__.py @@ -8,12 +8,14 @@ from amanuensis.server.helpers import lexicon_param, player_required_if_not_publ from .characters import bp as characters_bp from .forms import LexiconJoinForm +from .settings import bp as settings_bp bp = Blueprint( "lexicon", __name__, url_prefix="/lexicon/<lexicon_name>", template_folder="." ) bp.register_blueprint(characters_bp) +bp.register_blueprint(settings_bp) @bp.route("/join/", methods=["GET", "POST"]) diff --git a/amanuensis/server/lexicon/settings/__init__.py b/amanuensis/server/lexicon/settings/__init__.py new file mode 100644 index 0000000..9705e58 --- /dev/null +++ b/amanuensis/server/lexicon/settings/__init__.py @@ -0,0 +1,65 @@ +from flask import Blueprint, render_template, url_for, redirect + +from amanuensis.backend import * +from amanuensis.db import * +from amanuensis.server.helpers import editor_required, lexicon_param, player_required + + +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.get("/player/") +@lexicon_param +@player_required +def player(lexicon_name): + return render_template("settings.jinja", lexicon_name=lexicon_name, page_name=player.__name__) + + +@bp.get("/general/") +@lexicon_param +@editor_required +def general(lexicon_name): + return render_template( + "settings.jinja", lexicon_name=lexicon_name, page_name=general.__name__ + ) + + +@bp.get("/join/") +@lexicon_param +@editor_required +def join(lexicon_name): + return render_template("settings.jinja", lexicon_name=lexicon_name, page_name=join.__name__) + + +@bp.get("/progress/") +@lexicon_param +@editor_required +def progress(lexicon_name): + return render_template( + "settings.jinja", lexicon_name=lexicon_name, page_name=progress.__name__ + ) + + +@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/settings.jinja b/amanuensis/server/lexicon/settings/settings.jinja new file mode 100644 index 0000000..6cfa2ce --- /dev/null +++ b/amanuensis/server/lexicon/settings/settings.jinja @@ -0,0 +1,45 @@ +{% extends "lexicon.jinja" %} +{% block title %}Edit | {{ lexicon_title }}{% endblock %} + +{% macro settings_page_link(page, text) -%} +<a{% if page_name != page %} href="{{ url_for('lexicon.settings.' + page, lexicon_name=lexicon_name) }}"{% endif %}>{{ text }}</a> +{%- endmacro %} + +{% block main %} +{% if current_membership.is_editor %} +<ul class="unordered-tabs"> + <li>{{ settings_page_link("player", "Player") }}</li> + <li>{{ settings_page_link("general", "General") }}</li> + <li>{{ settings_page_link("join", "Visibility and Joining") }}</li> + <li>{{ settings_page_link("progress", "Game Progress") }}</li> + <li>{{ settings_page_link("publish", "Turn Publishing") }}</li> + <li>{{ settings_page_link("article", "Article Requirements") }}</li> +</ul> +{% endif %} + +{% if page_name == "player" %} + <h3>Player Settings</h3> +{% endif %} + +{% if page_name == "general" %} + <h3>General Settings</h3> +{% endif %} + +{% if page_name == "join" %} + <h3>Visibility and Joining</h3> +{% endif %} + +{% if page_name == "progress" %} + <h3>Game Progress</h3> +{% endif %} + +{% if page_name == "publish" %} + <h3>Turn Publishing</h3> +{% endif %} + +{% if page_name == "article" %} + <h3>Article Requirements</h3> +{% endif %} +{% endblock %} + +{% set template_content_blocks = [self.main()] %} -- 2.44.1 From bff25fb97f1f3e626d6d4d94afe491c70320bf2d Mon Sep 17 00:00:00 2001 From: Tim Van Baak <tim.vanbaak@gmail.com> Date: Sat, 4 Sep 2021 13:48:24 -0700 Subject: [PATCH 5/8] Implement player-specific and game setup settings --- amanuensis/backend/lexicon.py | 6 +- amanuensis/resources/page.css | 4 + .../server/lexicon/characters/__init__.py | 2 +- .../server/lexicon/settings/__init__.py | 108 +++++++++++++++--- amanuensis/server/lexicon/settings/forms.py | 45 ++++++++ .../server/lexicon/settings/settings.jinja | 70 ++++++++++-- 6 files changed, 211 insertions(+), 24 deletions(-) create mode 100644 amanuensis/server/lexicon/settings/forms.py diff --git a/amanuensis/backend/lexicon.py b/amanuensis/backend/lexicon.py index d2dab71..233c427 100644 --- a/amanuensis/backend/lexicon.py +++ b/amanuensis/backend/lexicon.py @@ -84,7 +84,11 @@ def password_check(db: DbContext, lexicon_id: int, password: str) -> bool: def password_set(db: DbContext, lexicon_id: int, new_password: Optional[str]) -> None: """Set or clear a lexicon's password.""" password_hash = generate_password_hash(new_password) if new_password else None - db(update(Lexicon).where(Lexicon.id == lexicon_id).values(password=password_hash)) + db( + update(Lexicon) + .where(Lexicon.id == lexicon_id) + .values(join_password=password_hash) + ) db.session.commit() diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index db95940..c798e74 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -114,6 +114,10 @@ textarea.fullwidth { width: 100%; box-sizing: border-box; } +input.fullwidth { + width: 100%; + box-sizing: border-box; +} input.smallnumber { width: 4em; } diff --git a/amanuensis/server/lexicon/characters/__init__.py b/amanuensis/server/lexicon/characters/__init__.py index 0a3483c..56d5a0d 100644 --- a/amanuensis/server/lexicon/characters/__init__.py +++ b/amanuensis/server/lexicon/characters/__init__.py @@ -50,7 +50,7 @@ def edit(lexicon_name, character_id: uuid.UUID): # Data is valid character.name = form.name.data character.signature = form.signature.data - g.db.session.commit() + g.db.session.commit() # TODO refactor into backend return redirect( url_for("lexicon.characters.list", lexicon_name=lexicon_name) ) diff --git a/amanuensis/server/lexicon/settings/__init__.py b/amanuensis/server/lexicon/settings/__init__.py index 9705e58..3a161a9 100644 --- a/amanuensis/server/lexicon/settings/__init__.py +++ b/amanuensis/server/lexicon/settings/__init__.py @@ -1,8 +1,16 @@ -from flask import Blueprint, render_template, url_for, redirect +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 +from amanuensis.server.helpers import ( + editor_required, + lexicon_param, + player_required, + current_membership, + current_lexicon, +) + +from .forms import PlayerSettingsForm, SetupSettingsForm bp = Blueprint("settings", __name__, url_prefix="/settings", template_folder=".") @@ -15,27 +23,99 @@ def page(lexicon_name): return redirect(url_for("lexicon.settings.player", lexicon_name=lexicon_name)) -@bp.get("/player/") +@bp.route("/player/", methods=["GET", "POST"]) @lexicon_param @player_required def player(lexicon_name): - return render_template("settings.jinja", lexicon_name=lexicon_name, page_name=player.__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.get("/general/") +@bp.route("/setup/", methods=["GET", "POST"]) @lexicon_param @editor_required -def general(lexicon_name): - return render_template( - "settings.jinja", lexicon_name=lexicon_name, page_name=general.__name__ - ) +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, + ) -@bp.get("/join/") -@lexicon_param -@editor_required -def join(lexicon_name): - return render_template("settings.jinja", lexicon_name=lexicon_name, page_name=join.__name__) + 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("/progress/") diff --git a/amanuensis/server/lexicon/settings/forms.py b/amanuensis/server/lexicon/settings/forms.py new file mode 100644 index 0000000..612a7af --- /dev/null +++ b/amanuensis/server/lexicon/settings/forms.py @@ -0,0 +1,45 @@ +from flask_wtf import FlaskForm +from wtforms import ( + BooleanField, + IntegerField, + PasswordField, + StringField, + SubmitField, + TextAreaField, +) +from wtforms.validators import Optional, DataRequired +from wtforms.widgets.html5 import NumberInput + + +class PlayerSettingsForm(FlaskForm): + """/lexicon/<name>/settings/player/""" + + notify_ready = BooleanField("Notify me when an article is submitted for review") + notify_reject = BooleanField("Notify me when an editor rejects one of my articles") + notify_approve = BooleanField( + "Notify me when an editor approves one of my articles" + ) + submit = SubmitField("Submit") + + +class SetupSettingsForm(FlaskForm): + """/lexicon/<name>/settings/setup/""" + + title = StringField("Title override") + prompt = TextAreaField("Prompt", validators=[DataRequired()]) + public = BooleanField("Make game publicly visible") + joinable = BooleanField("Allow players to join game") + has_password = BooleanField("Require password to join the game") + password = PasswordField("Game password") + turn_count = IntegerField( + "Number of turns", widget=NumberInput(), validators=[DataRequired()] + ) + player_limit = IntegerField( + "Maximum number of players", widget=NumberInput(), validators=[Optional()] + ) + character_limit = IntegerField( + "Maximum number of characters per player", + widget=NumberInput(), + validators=[Optional()], + ) + submit = SubmitField("Submit") diff --git a/amanuensis/server/lexicon/settings/settings.jinja b/amanuensis/server/lexicon/settings/settings.jinja index 6cfa2ce..3c4de9e 100644 --- a/amanuensis/server/lexicon/settings/settings.jinja +++ b/amanuensis/server/lexicon/settings/settings.jinja @@ -5,12 +5,24 @@ <a{% if page_name != page %} href="{{ url_for('lexicon.settings.' + page, lexicon_name=lexicon_name) }}"{% endif %}>{{ text }}</a> {%- endmacro %} +{% macro flag_setting(field) %} +{{ field() }} +{{ field.label }}<br> +{% endmacro %} + +{% 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 %} + {% block main %} {% if current_membership.is_editor %} <ul class="unordered-tabs"> <li>{{ settings_page_link("player", "Player") }}</li> - <li>{{ settings_page_link("general", "General") }}</li> - <li>{{ settings_page_link("join", "Visibility and Joining") }}</li> + <li>{{ settings_page_link("setup", "Game Setup") }}</li> <li>{{ settings_page_link("progress", "Game Progress") }}</li> <li>{{ settings_page_link("publish", "Turn Publishing") }}</li> <li>{{ settings_page_link("article", "Article Requirements") }}</li> @@ -19,14 +31,56 @@ {% if page_name == "player" %} <h3>Player Settings</h3> + <form action="" method="post" novalidate> + {{ form.hidden_tag() }} + <p> + {% if current_membership.is_editor %}{{ flag_setting(form.notify_ready) }}{% endif %} + {{ flag_setting(form.notify_reject) }} + {{ flag_setting(form.notify_approve) }} + </p> + <p>{{ form.submit() }}</p> + </form> + + {% for message in get_flashed_messages() %} + <span style="color:#ff0000">{{ message }}</span><br> + {% endfor %} {% endif %} -{% if page_name == "general" %} - <h3>General Settings</h3> -{% endif %} - -{% if page_name == "join" %} - <h3>Visibility and Joining</h3> +{% if page_name == "setup" %} + <h3>Game Setup</h3> + <form action="" method="post" novalidate> + {{ form.hidden_tag() }} + <p> + {{ form.title.label }}:<br> + {{ form.title(autocomplete="off", placeholder="Lexicon " + lexicon_name, class_="fullwidth") }}<br> + </p> + <p> + {{ form.prompt.label }}: {{ form.prompt(class_="fullwidth") }} + {% for error in form.prompt.errors %} + <span style="color: #ff0000">{{ error }}</span><br> + {% endfor %} + </p> + <p> + {{ flag_setting(form.public) }} + {{ flag_setting(form.joinable) }} + {{ form.has_password() }} + {{ form.has_password.label }}:<br> + {{ form.password(autocomplete="off") }} + </p> + <p> + {{ number_setting(form.turn_count) }} + </p> + <p> + {{ number_setting(form.player_limit) }} + </p> + <p> + {{ number_setting(form.character_limit) }} + </p> + <p>{{ form.submit() }}</p> + </form> + {% for message in get_flashed_messages() %} + <span style="color:#ff0000">{{ message }}</span><br> + {% endfor %} {% endif %} {% if page_name == "progress" %} -- 2.44.1 From 03c0b4ce70014c8d0fb323a63254d573be23dcc5 Mon Sep 17 00:00:00 2001 From: Tim Van Baak <tim.vanbaak@gmail.com> Date: Sat, 4 Sep 2021 20:46:34 -0700 Subject: [PATCH 6/8] Add index cli and create command --- amanuensis/cli/__init__.py | 2 ++ amanuensis/cli/index.py | 42 ++++++++++++++++++++++++++++++++++++++ amanuensis/db/models.py | 3 +++ 3 files changed, 47 insertions(+) create mode 100644 amanuensis/cli/index.py diff --git a/amanuensis/cli/__init__.py b/amanuensis/cli/__init__.py index f1d0807..e457db4 100644 --- a/amanuensis/cli/__init__.py +++ b/amanuensis/cli/__init__.py @@ -6,6 +6,7 @@ from typing import Callable import amanuensis.cli.admin import amanuensis.cli.character +import amanuensis.cli.index import amanuensis.cli.lexicon import amanuensis.cli.user from amanuensis.db import DbContext @@ -110,6 +111,7 @@ def main(): subparsers = parser.add_subparsers(metavar="COMMAND") add_subcommand(subparsers, amanuensis.cli.admin) add_subcommand(subparsers, amanuensis.cli.character) + add_subcommand(subparsers, amanuensis.cli.index) add_subcommand(subparsers, amanuensis.cli.lexicon) add_subcommand(subparsers, amanuensis.cli.user) diff --git a/amanuensis/cli/index.py b/amanuensis/cli/index.py new file mode 100644 index 0000000..3b09df0 --- /dev/null +++ b/amanuensis/cli/index.py @@ -0,0 +1,42 @@ +import enum +import logging + +from amanuensis.backend import * +from amanuensis.db import DbContext, ArticleIndex, IndexType + +from .helpers import add_argument + + +COMMAND_NAME = "index" +COMMAND_HELP = "Interact with indexes." + +LOG = logging.getLogger(__name__) + + +@add_argument("--lexicon", required=True) +@add_argument( + "--type", required=True, type=lambda s: IndexType[s.upper()], choices=IndexType +) +@add_argument("--pattern", required=True) +@add_argument("--logical", type=int, default=0) +@add_argument("--display", type=int, default=0) +@add_argument("--capacity", type=int, default=None) +def command_create(args) -> int: + """ + Create an index for a lexicon. + """ + db: DbContext = args.get_db() + lexicon = lexiq.try_from_name(db, args.lexicon) + if not lexicon: + raise ValueError("Lexicon does not exist") + index: ArticleIndex = indq.create( + db, + lexicon.id, + args.type, + args.pattern, + args.logical, + args.display, + args.capacity, + ) + LOG.info(f"Created {index.index_type}:{index.pattern} in {lexicon.full_title}") + return 0 diff --git a/amanuensis/db/models.py b/amanuensis/db/models.py index 2e63c4c..c9461a9 100644 --- a/amanuensis/db/models.py +++ b/amanuensis/db/models.py @@ -463,6 +463,9 @@ class IndexType(enum.Enum): PREFIX = 2 ETC = 3 + def __str__(self): + return self.name + class ArticleIndex(ModelBase): """ -- 2.44.1 From e353ac9b9396057389d21e7bf18011f02c84adda Mon Sep 17 00:00:00 2001 From: Tim Van Baak <tim.vanbaak@gmail.com> Date: Mon, 6 Sep 2021 22:01:08 -0700 Subject: [PATCH 7/8] Add index settings page Unlike the player and setup settings, the form here has a variable number of inputs, so we use a blank row to allow expanding the index set and allow deleting by clearing out the index type --- amanuensis/backend/index.py | 53 ++++++++++- amanuensis/db/models.py | 1 + amanuensis/resources/page.css | 22 +++-- amanuensis/server/__init__.py | 1 + .../server/lexicon/settings/__init__.py | 60 +++++++++++- amanuensis/server/lexicon/settings/forms.py | 54 ++++++++++- .../server/lexicon/settings/settings.jinja | 49 +++++++++- tests/test_character.py | 2 - tests/test_index.py | 91 +++++++++++++++++++ 9 files changed, 313 insertions(+), 20 deletions(-) create mode 100644 tests/test_index.py diff --git a/amanuensis/backend/index.py b/amanuensis/backend/index.py index bfd259c..e1a64be 100644 --- a/amanuensis/backend/index.py +++ b/amanuensis/backend/index.py @@ -3,7 +3,9 @@ Index query interface """ import re -from typing import Optional +from typing import Optional, Sequence + +from sqlalchemy import select from amanuensis.db import DbContext, ArticleIndex, IndexType from amanuensis.errors import ArgumentError, BackendArgumentTypeError @@ -72,3 +74,52 @@ def create( db.session.add(new_index) db.session.commit() return new_index + + +def get_for_lexicon(db: DbContext, lexicon_id: int) -> Sequence[ArticleIndex]: + """Returns all index rules for a lexicon.""" + return db( + select(ArticleIndex).where(ArticleIndex.lexicon_id == lexicon_id) + ).scalars() + + + +def update(db: DbContext, lexicon_id: int, indices: Sequence[ArticleIndex]) -> None: + """ + Update the indices for a lexicon. Indices are matched by type and pattern. + An extant index not matched to an input is deleted, and an input index not + matched to a an extant index is created. Matched indexes are updated with + the input logical and display orders and capacity. + """ + extant_indices: Sequence[ArticleIndex] = list(get_for_lexicon(db, lexicon_id)) + s = lambda i: f"{i.index_type}:{i.pattern}" + for extant_index in extant_indices: + match = None + for new_index in indices: + is_match = ( + extant_index.index_type == new_index.index_type + and extant_index.pattern == new_index.pattern + ) + if is_match: + match = new_index + break + if match: + extant_index.logical_order = new_index.logical_order + extant_index.display_order = new_index.display_order + extant_index.capacity = new_index.capacity + else: + db.session.delete(extant_index) + for new_index in indices: + match = None + for extant_index in extant_indices: + is_match = ( + extant_index.index_type == new_index.index_type + and extant_index.pattern == new_index.pattern + ) + if is_match: + match = extant_index + break + if not match: + new_index.lexicon_id = lexicon_id + db.session.add(new_index) + db.session.commit() diff --git a/amanuensis/db/models.py b/amanuensis/db/models.py index c9461a9..e972e31 100644 --- a/amanuensis/db/models.py +++ b/amanuensis/db/models.py @@ -473,6 +473,7 @@ class ArticleIndex(ModelBase): """ __tablename__ = "article_index" + __table_args__ = (UniqueConstraint("lexicon_id", "index_type", "pattern"),) ############## # Index info # diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index c798e74..976f2f8 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -46,11 +46,8 @@ div#sidebar { img#logo { max-width: 200px; } -table { - table-layout: fixed; - width: 100%; -} div#sidebar table { + width: 100%; border-collapse: collapse; } div.citeblock table td:first-child + td a { @@ -118,9 +115,6 @@ input.fullwidth { width: 100%; box-sizing: border-box; } -input.smallnumber { - width: 4em; -} form#session-settings p { line-height: 1.8em; } @@ -207,6 +201,20 @@ ul.unordered-tabs li a[href]:hover { background-color: var(--button-hover); border-color: var(--button-hover); } +#index-definition-help { + margin-block-start: 1em; + margin-block-end: 1em; +} +#index-definition-table td:nth-child(2) { + width: 100%; +} +#index-definition-table td:nth-child(2) *:only-child { + box-sizing: border-box; + width: 100%; +} +#index-definition-table td input[type=number] { + width: 4em; +} @media only screen and (max-width: 816px) { div#wrapper { padding: 5px; diff --git a/amanuensis/server/__init__.py b/amanuensis/server/__init__.py index bb0e0b4..c8a9827 100644 --- a/amanuensis/server/__init__.py +++ b/amanuensis/server/__init__.py @@ -75,6 +75,7 @@ def get_app( "userq": userq, "memq": memq, "charq": charq, + "indq": indq, "current_lexicon": current_lexicon, "current_membership": current_membership } diff --git a/amanuensis/server/lexicon/settings/__init__.py b/amanuensis/server/lexicon/settings/__init__.py index 3a161a9..26b4558 100644 --- a/amanuensis/server/lexicon/settings/__init__.py +++ b/amanuensis/server/lexicon/settings/__init__.py @@ -1,3 +1,5 @@ +from typing import Sequence + from flask import Blueprint, render_template, url_for, g, flash, redirect from amanuensis.backend import * @@ -10,7 +12,7 @@ from amanuensis.server.helpers import ( current_lexicon, ) -from .forms import PlayerSettingsForm, SetupSettingsForm +from .forms import PlayerSettingsForm, SetupSettingsForm, IndexSchemaForm bp = Blueprint("settings", __name__, url_prefix="/settings", template_folder=".") @@ -118,13 +120,61 @@ def setup(lexicon_name): ) -@bp.get("/progress/") +@bp.get("/index/") @lexicon_param @editor_required -def progress(lexicon_name): - return render_template( - "settings.jinja", lexicon_name=lexicon_name, page_name=progress.__name__ +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/") diff --git a/amanuensis/server/lexicon/settings/forms.py b/amanuensis/server/lexicon/settings/forms.py index 612a7af..7b9209c 100644 --- a/amanuensis/server/lexicon/settings/forms.py +++ b/amanuensis/server/lexicon/settings/forms.py @@ -1,15 +1,20 @@ from flask_wtf import FlaskForm from wtforms import ( BooleanField, + FieldList, + FormField, IntegerField, PasswordField, + SelectField, StringField, SubmitField, TextAreaField, ) -from wtforms.validators import Optional, DataRequired +from wtforms.validators import Optional, DataRequired, ValidationError from wtforms.widgets.html5 import NumberInput +from amanuensis.db import ArticleIndex, IndexType + class PlayerSettingsForm(FlaskForm): """/lexicon/<name>/settings/player/""" @@ -43,3 +48,50 @@ class SetupSettingsForm(FlaskForm): validators=[Optional()], ) submit = SubmitField("Submit") + + +def parse_index_type(type_str): + if not type_str: + return None + return getattr(IndexType, type_str) + + +class IndexDefinitionForm(FlaskForm): + """/lexicon/<name>/settings/index/""" + + class Meta: + # Disable CSRF on the individual index definitions, since the schema + # form will have one + csrf = False + + TYPE_CHOICES = [("", "")] + [(str(t), str(t).lower()) for t in IndexType] + + index_type = SelectField(choices=TYPE_CHOICES, coerce=parse_index_type) + pattern = StringField() + logical_order = IntegerField( + widget=NumberInput(min=-99, max=99), validators=[Optional()] + ) + display_order = IntegerField( + widget=NumberInput(min=-99, max=99), validators=[Optional()] + ) + capacity = IntegerField(widget=NumberInput(min=0, max=99), validators=[Optional()]) + + def validate_pattern(form, field): + if form.index_type.data and not field.data: + raise ValidationError("Pattern must be defined") + + def to_model(self): + return ArticleIndex( + index_type=self.index_type.data, + pattern=self.pattern.data, + logical_order=self.logical_order.data, + display_order=self.display_order.data, + capacity=self.capacity.data, + ) + + +class IndexSchemaForm(FlaskForm): + """/lexicon/<name>/settings/index/""" + + indices = FieldList(FormField(IndexDefinitionForm)) + submit = SubmitField("Submit") diff --git a/amanuensis/server/lexicon/settings/settings.jinja b/amanuensis/server/lexicon/settings/settings.jinja index 3c4de9e..d17d712 100644 --- a/amanuensis/server/lexicon/settings/settings.jinja +++ b/amanuensis/server/lexicon/settings/settings.jinja @@ -21,9 +21,9 @@ {% block main %} {% if current_membership.is_editor %} <ul class="unordered-tabs"> - <li>{{ settings_page_link("player", "Player") }}</li> + <li>{{ settings_page_link("player", "Player Settings") }}</li> <li>{{ settings_page_link("setup", "Game Setup") }}</li> - <li>{{ settings_page_link("progress", "Game Progress") }}</li> + <li>{{ settings_page_link("index", "Article Indices") }}</li> <li>{{ settings_page_link("publish", "Turn Publishing") }}</li> <li>{{ settings_page_link("article", "Article Requirements") }}</li> </ul> @@ -31,6 +31,7 @@ {% if page_name == "player" %} <h3>Player Settings</h3> + <p>These settings are specific to you as a player in this lexicon.</p> <form action="" method="post" novalidate> {{ form.hidden_tag() }} <p> @@ -83,8 +84,48 @@ {% endfor %} {% endif %} -{% if page_name == "progress" %} - <h3>Game Progress</h3> +{% if page_name == "index" %} + <h3>Article Indexes</h3> + <details id="index-definition-help"> + <summary>Index definition help</summary> + <p>An index is a rule that matches the title of a lexicon article based on its <em>index type</em> and <em>pattern</em>. A <em>char</em> 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 <em>range</em> 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 <em>prefix</em> index matches any title that begins with the pattern. An <em>etc</em> index always matches a title.</p> + <p>When a title is to be sorted under an index, indices are checked in order, sorted first by descending order of <em>logical priority</em>, and then by alphabetical order of index pattern. The title is sorted under the first index that matches it.</p> + <p>On the contents page, indices and the articles under them are displayed sorted instead by <em>display order</em> and then alphabetically by pattern.</p> + <p>The <em>capacity</em> 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.</p> + <p>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.</p> + </details> + <form action="" method="post" novalidate> + {{ form.hidden_tag() }} + <table id="index-definition-table"> + <tr> + <th>Type</th> + <th>Pattern</th> + <th>Disp Or</th> + <th>Log Or</th> + <th>Cap</th> + </tr> + {% for index_form in form.indices %} + <tr> + <td>{{ index_form.index_type() }}</td> + <td>{{ index_form.pattern() }}</td> + <td>{{ index_form.logical_order() }}</td> + <td>{{ index_form.display_order() }}</td> + <td>{{ index_form.capacity() }}</td> + </tr> + {% for field in index_form %} + {% for error in field.errors %} + <tr> + <td colspan="5"><span style="color: #ff0000">{{ error }}</span></td> + </tr> + {% endfor %} + {% endfor %} + {% endfor %} + </table> + <p>{{ form.submit() }}</p> + </form> + {% for message in get_flashed_messages() %} + <span style="color:#ff0000">{{ message }}</span><br> + {% endfor %} {% endif %} {% if page_name == "publish" %} diff --git a/tests/test_character.py b/tests/test_character.py index 176bbc5..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 @@ -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" -- 2.44.1 From 4d1c579e3cf2bb1b4c8794ce1092b5db3a10fae6 Mon Sep 17 00:00:00 2001 From: Tim Van Baak <tim.vanbaak@gmail.com> Date: Thu, 9 Sep 2021 18:10:05 -0700 Subject: [PATCH 8/8] Remove obsolete settings These settings have now been reintegrated into the new code, so we can remove them from the old code to make it easier to audit what has and hasn't been converted yet --- .../server/session/session.settings.jinja | 26 --------- amanuensis/server/session/settings.py | 57 ------------------- 2 files changed, 83 deletions(-) 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 @@ <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) }} - {% for error in form.articleIndexList.errors %} - <span style="color: #ff0000">{{ error }}</span><br> - {% endfor %} - {{ number_setting(form.articleIndexCapacity) }} {{ form.turnAssignment.label }}:<br> {{ form.turnAssignment(class_="fullwidth", rows=10) }} </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) }} - {{ number_setting(form.joinCharsPerPlayer) }} - </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) }} 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')) -- 2.44.1