From c26a0a058b0557af3d4e53029569fbbe019cf5db Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 19 Aug 2021 18:10:00 -0700 Subject: [PATCH 1/9] Print help instead of only usage --- amanuensis/cli/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amanuensis/cli/__init__.py b/amanuensis/cli/__init__.py index eb9e111..1fa9234 100644 --- a/amanuensis/cli/__init__.py +++ b/amanuensis/cli/__init__.py @@ -50,7 +50,7 @@ def add_subcommand(subparsers, module) -> None: command_parser: ArgumentParser = subparsers.add_parser( command_name, help=command_help ) - command_parser.set_defaults(func=lambda args: command_parser.print_usage()) + command_parser.set_defaults(func=lambda args: command_parser.print_help()) # Add all subcommands in the command module subcommands = command_parser.add_subparsers(metavar="SUBCOMMAND") @@ -97,7 +97,7 @@ def main(): parser = ArgumentParser() parser.set_defaults( parser=parser, - func=lambda args: parser.print_usage(), + func=lambda args: parser.print_help(), get_db=None, ) parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") -- 2.44.1 From e480658ebe01d0629202f33fd0429cb404797da6 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 19 Aug 2021 18:17:06 -0700 Subject: [PATCH 2/9] Add character create command --- amanuensis/cli/__init__.py | 2 ++ amanuensis/cli/character.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 amanuensis/cli/character.py diff --git a/amanuensis/cli/__init__.py b/amanuensis/cli/__init__.py index 1fa9234..f1d0807 100644 --- a/amanuensis/cli/__init__.py +++ b/amanuensis/cli/__init__.py @@ -5,6 +5,7 @@ import os from typing import Callable import amanuensis.cli.admin +import amanuensis.cli.character import amanuensis.cli.lexicon import amanuensis.cli.user from amanuensis.db import DbContext @@ -108,6 +109,7 @@ def main(): # Add commands from cli submodules subparsers = parser.add_subparsers(metavar="COMMAND") add_subcommand(subparsers, amanuensis.cli.admin) + add_subcommand(subparsers, amanuensis.cli.character) add_subcommand(subparsers, amanuensis.cli.lexicon) add_subcommand(subparsers, amanuensis.cli.user) diff --git a/amanuensis/cli/character.py b/amanuensis/cli/character.py new file mode 100644 index 0000000..f49fe61 --- /dev/null +++ b/amanuensis/cli/character.py @@ -0,0 +1,31 @@ +import logging + +from amanuensis.backend import lexiq, userq, charq +from amanuensis.db import DbContext, Character + +from .helpers import add_argument + + +COMMAND_NAME = "char" +COMMAND_HELP = "Interact with characters." + +LOG = logging.getLogger(__name__) + + +@add_argument("--lexicon", required=True) +@add_argument("--user", required=True) +@add_argument("--name", required=True) +def command_create(args) -> int: + """ + Create a character. + """ + db: DbContext = args.get_db() + lexicon = lexiq.try_from_name(db, args.lexicon) + if not lexicon: + raise ValueError("Lexicon does not exist") + user = userq.try_from_username(db, args.user) + if not user: + raise ValueError("User does not exist") + char: Character = charq.create(db, lexicon.id, user.id, args.name, signature=None) + LOG.info(f"Created {char.name} in {lexicon.full_title}") + return 0 -- 2.44.1 From c6f3ae4779a716d79bb41ab6c35d47c683919244 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 27 Aug 2021 05:15:13 -0700 Subject: [PATCH 3/9] Add top-level character page --- amanuensis/backend/character.py | 7 +- amanuensis/backend/membership.py | 7 ++ amanuensis/resources/page.css | 13 +++ amanuensis/server/__init__.py | 4 +- amanuensis/server/lexicon.jinja | 6 ++ amanuensis/server/lexicon/__init__.py | 2 + .../server/lexicon/characters/__init__.py | 94 +++++++++++++++++++ .../lexicon/characters/characters.jinja | 37 ++++++++ amanuensis/server/session/__init__.py | 71 -------------- .../server/session/session.character.jinja | 26 ----- 10 files changed, 167 insertions(+), 100 deletions(-) create mode 100644 amanuensis/server/lexicon/characters/__init__.py create mode 100644 amanuensis/server/lexicon/characters/characters.jinja delete mode 100644 amanuensis/server/session/session.character.jinja diff --git a/amanuensis/backend/character.py b/amanuensis/backend/character.py index 01f5804..2090af7 100644 --- a/amanuensis/backend/character.py +++ b/amanuensis/backend/character.py @@ -2,7 +2,7 @@ Character query interface """ -from typing import Optional +from typing import Optional, Sequence from sqlalchemy import select, func @@ -68,3 +68,8 @@ def create( db.session.add(new_character) db.session.commit() return new_character + + +def get_in_lexicon(db: DbContext, lexicon_id: int) -> Sequence[Character]: + """Get all characters in a lexicon.""" + return db(select(Character).where(Character.lexicon_id == lexicon_id)).scalars() \ No newline at end of file diff --git a/amanuensis/backend/membership.py b/amanuensis/backend/membership.py index dff50e2..2dd08b8 100644 --- a/amanuensis/backend/membership.py +++ b/amanuensis/backend/membership.py @@ -2,6 +2,8 @@ Membership query interface """ +from typing import Sequence + from sqlalchemy import select, func from amanuensis.db import DbContext, Membership @@ -66,6 +68,11 @@ def create( return new_membership +def get_players_in_lexicon(db: DbContext, lexicon_id: int) -> Sequence[Membership]: + """Get all users who are members of a lexicon.""" + return db(select(Membership).where(Membership.lexicon_id == lexicon_id)).scalars() + + def try_from_ids(db: DbContext, user_id: int, lexicon_id: int) -> Membership: """Get a membership by the user and lexicon ids, or None if no such membership was found.""" return db( diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index b22db7b..5f3f22d 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -126,6 +126,19 @@ div.dashboard-lexicon-item { padding: 0 10px; border-left: 3px solid black; } +ul.blockitem-list { + list-style: none; + margin-block-start: 1em; + margin-block-end: 1em; + margin-inline-start: 0.5em; + margin-inline-end: 0.5em; + padding-inline-start: 0; + padding-inline-end: 0; +} +ul.blockitem-list li { + border-inline-start: 3px solid black; + padding-inline-start: 0.5em; +} div.dashboard-lexicon-unstarted { border-left-color: blue; } diff --git a/amanuensis/server/__init__.py b/amanuensis/server/__init__.py index 5e845f8..8f5c842 100644 --- a/amanuensis/server/__init__.py +++ b/amanuensis/server/__init__.py @@ -4,7 +4,7 @@ import os from flask import Flask, g, url_for, redirect -from amanuensis.backend import lexiq, userq, memq +from amanuensis.backend import * from amanuensis.config import AmanuensisConfig, CommandLineConfig from amanuensis.db import DbContext from amanuensis.parser import filesafe_title @@ -68,7 +68,7 @@ def get_app( # Configure jinja options def include_backend(): - return {"db": db, "lexiq": lexiq, "userq": userq, "memq": memq} + return {"db": db, "lexiq": lexiq, "userq": userq, "memq": memq, "charq": charq} app.jinja_options.update(trim_blocks=True, lstrip_blocks=True) app.template_filter("date")(date_format) diff --git a/amanuensis/server/lexicon.jinja b/amanuensis/server/lexicon.jinja index 9c976eb..8393fc1 100644 --- a/amanuensis/server/lexicon.jinja +++ b/amanuensis/server/lexicon.jinja @@ -9,6 +9,10 @@ {% block sb_logo %}{% endblock %} {% block sb_home %}Home {% endblock %} +{% block sb_characters %}Characters{% endblock %} {% block sb_contents %}= g.lexicon.cfg.join.chars_per_player: +# flash("Can't create more characters") +# return redirect(url_for('session.session', name=name)) + +# if not form.is_submitted(): +# # GET, populate with default values +# return render_template( +# 'session.character.jinja', form=form.for_new()) + +# if not form.validate(): +# # POST with invalid data, return unchanged +# return render_template('session.character.jinja', form=form) + +# # POST with valid data, create character +# char_name = form.characterName.data +# cid = create_character_in_lexicon(current_user, g.lexicon, char_name) +# with g.lexicon.ctx.edit_config() as cfg: +# cfg.character[cid].signature = form.defaultSignature.data +# flash('Character created') +# return redirect(url_for('session.session', name=name)) + diff --git a/amanuensis/server/lexicon/characters/characters.jinja b/amanuensis/server/lexicon/characters/characters.jinja new file mode 100644 index 0000000..72678b0 --- /dev/null +++ b/amanuensis/server/lexicon/characters/characters.jinja @@ -0,0 +1,37 @@ +{% extends "lexicon.jinja" %} +{% block title %}Character | {{ lexicon_title }}{% endblock %} + +{% block main %} + +

Characters

+{% set players = memq.get_players_in_lexicon(db, g.lexicon.id)|list %} +{% set characters = charq.get_in_lexicon(db, g.lexicon.id)|list %} +

This lexicon has {{ players|count }} player{% if players|count > 1 %}s{% endif %} and {{ characters|count }} character{% if characters|count > 1 %}s{% endif %}.

+
    +{% for character in characters %} +
  • +

    {{ character.name }}

    +

    Player: {{ character.user.username }}

    +
  • +{% endfor %} +
+{#
+ {{ form.hidden_tag() }} +

+ {{ form.characterName.label }}
{{ form.characterName(size=32) }} +

+ {% for error in form.characterName.errors %} + {{ error }}
+ {% endfor %}

+

+ {{ form.defaultSignature.label }}
{{ form.defaultSignature(class_='fullwidth') }} +

+

{{ form.submit() }}

+
#} + +{# {% for message in get_flashed_messages() %} +{{ message }}
+{% endfor %} #} + +{% endblock %} +{% set template_content_blocks = [self.main()] %} \ No newline at end of file diff --git a/amanuensis/server/session/__init__.py b/amanuensis/server/session/__init__.py index 743754d..2f1fdff 100644 --- a/amanuensis/server/session/__init__.py +++ b/amanuensis/server/session/__init__.py @@ -68,77 +68,6 @@ def session(name): publish_form=form) -def edit_character(name, form, character): - if not form.is_submitted(): - # GET, populate with values - return render_template( - 'session.character.jinja', form=form.for_character(character)) - - if not form.validate(): - # POST with invalid data, return unchanged - return render_template('session.character.jinja', form=form) - - # POST with valid data, update character - with g.lexicon.ctx.edit_config() as cfg: - char = cfg.character[character.cid] - char.name = form.characterName.data - char.signature = form.defaultSignature.data - flash('Character updated') - return redirect(url_for('session.session', name=name)) - - -def create_character(name: str, form: LexiconCharacterForm): - # Characters can't be created if the game has already started - if g.lexicon.status != LexiconModel.PREGAME: - flash("Characters can't be added after the game has started") - return redirect(url_for('session.session', name=name)) - # Characters can't be created beyond the per-player limit - player_characters = get_player_characters(g.lexicon, current_user.uid) - if len(list(player_characters)) >= g.lexicon.cfg.join.chars_per_player: - flash("Can't create more characters") - return redirect(url_for('session.session', name=name)) - - if not form.is_submitted(): - # GET, populate with default values - return render_template( - 'session.character.jinja', form=form.for_new()) - - if not form.validate(): - # POST with invalid data, return unchanged - return render_template('session.character.jinja', form=form) - - # POST with valid data, create character - char_name = form.characterName.data - cid = create_character_in_lexicon(current_user, g.lexicon, char_name) - with g.lexicon.ctx.edit_config() as cfg: - cfg.character[cid].signature = form.defaultSignature.data - flash('Character created') - return redirect(url_for('session.session', name=name)) - - -@bp_session.route('/character/', methods=['GET', 'POST']) -@lexicon_param -@player_required -def character(name): - form = LexiconCharacterForm() - cid = request.args.get('cid') - if not cid: - # No character specified, creating a new character - return create_character(name, form) - - character = g.lexicon.cfg.character.get(cid) - if not character: - # Bad character id, abort - flash('Character not found') - return redirect(url_for('session.session', name=name)) - if current_user.uid not in (character.player, g.lexicon.cfg.editor): - # Only its owner and the editor can edit a character - flash('Access denied') - return redirect(url_for('session.session', name=name)) - # Edit allowed - return edit_character(name, form, character) - - @bp_session.route('/settings/', methods=['GET', 'POST']) @lexicon_param @editor_required diff --git a/amanuensis/server/session/session.character.jinja b/amanuensis/server/session/session.character.jinja deleted file mode 100644 index 81b3036..0000000 --- a/amanuensis/server/session/session.character.jinja +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "lexicon.jinja" %} -{% block title %}Character | {{ lexicon_title }}{% endblock %} - -{% block main %} - -

Character

-
- {{ form.hidden_tag() }} -

- {{ form.characterName.label }}
{{ form.characterName(size=32) }} -

- {% for error in form.characterName.errors %} - {{ error }}
- {% endfor %}

-

- {{ form.defaultSignature.label }}
{{ form.defaultSignature(class_='fullwidth') }} -

-

{{ form.submit() }}

-
- -{% for message in get_flashed_messages() %} -{{ message }}
-{% endfor %} - -{% endblock %} -{% set template_content_blocks = [self.main()] %} \ No newline at end of file -- 2.44.1 From 3b95d650c109445f1af0ed1c43d764319039398e Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 27 Aug 2021 05:55:42 -0700 Subject: [PATCH 4/9] Replace home sidebar item with link next to login status --- amanuensis/server/lexicon.jinja | 4 ---- amanuensis/server/page.jinja | 7 ++++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/amanuensis/server/lexicon.jinja b/amanuensis/server/lexicon.jinja index 8393fc1..41ea53c 100644 --- a/amanuensis/server/lexicon.jinja +++ b/amanuensis/server/lexicon.jinja @@ -7,8 +7,6 @@ {% endblock %} {% block sb_logo %}{% endblock %} -{% block sb_home %}
Home -{% endblock %} {% block sb_characters %} {% if current_user.is_authenticated %} {{ current_user.username -}} - (Logout) + ‧ + Home + ‧ + Logout {% else %} + Home + ‧ Login {% endif %} -- 2.44.1 From af5b1c4cfa579cdb1320c73309a128a86010ea52 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 27 Aug 2021 07:49:47 -0700 Subject: [PATCH 5/9] Add character editing page --- amanuensis/backend/character.py | 8 +- amanuensis/resources/page.css | 4 + .../server/lexicon/characters/__init__.py | 79 +++++++++---------- .../lexicon/characters/characters.edit.jinja | 24 ++++++ .../lexicon/characters/characters.jinja | 27 ++----- amanuensis/server/lexicon/characters/forms.py | 11 +++ 6 files changed, 89 insertions(+), 64 deletions(-) create mode 100644 amanuensis/server/lexicon/characters/characters.edit.jinja create mode 100644 amanuensis/server/lexicon/characters/forms.py diff --git a/amanuensis/backend/character.py b/amanuensis/backend/character.py index 2090af7..c9be1fc 100644 --- a/amanuensis/backend/character.py +++ b/amanuensis/backend/character.py @@ -3,6 +3,7 @@ Character query interface """ from typing import Optional, Sequence +from uuid import UUID from sqlalchemy import select, func @@ -72,4 +73,9 @@ def create( def get_in_lexicon(db: DbContext, lexicon_id: int) -> Sequence[Character]: """Get all characters in a lexicon.""" - return db(select(Character).where(Character.lexicon_id == lexicon_id)).scalars() \ No newline at end of file + return db(select(Character).where(Character.lexicon_id == lexicon_id)).scalars() + + +def try_from_public_id(db: DbContext, public_id: UUID) -> Optional[Character]: + """Get a character by its public id.""" + return db(select(Character).where(Character.public_id == public_id)).scalar_one_or_none() diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index 5f3f22d..d32161f 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -139,6 +139,10 @@ ul.blockitem-list li { border-inline-start: 3px solid black; padding-inline-start: 0.5em; } +ul.blockitem-list p { + margin-block-start: 0.5em; + margin-block-end: 0.5em; +} div.dashboard-lexicon-unstarted { border-left-color: blue; } diff --git a/amanuensis/server/lexicon/characters/__init__.py b/amanuensis/server/lexicon/characters/__init__.py index 8504551..5276ce2 100644 --- a/amanuensis/server/lexicon/characters/__init__.py +++ b/amanuensis/server/lexicon/characters/__init__.py @@ -1,8 +1,15 @@ -from flask import Blueprint, render_template, url_for +from typing import Optional +import uuid + +from flask import Blueprint, render_template, url_for, g, flash from werkzeug.utils import redirect +from amanuensis.backend import charq +from amanuensis.db import Character from amanuensis.server.helpers import lexicon_param, player_required +from .forms import CharacterCreateForm + bp = Blueprint("characters", __name__, url_prefix="/characters", template_folder=".") @@ -14,54 +21,40 @@ def characters(name): return render_template('characters.jinja') -@bp.post('/') +@bp.route('/edit/', methods=['GET', 'POST']) @lexicon_param @player_required -def update(name): - return redirect(url_for('lexicon.statistics', name=name)) +def edit(name, character_id): + try: + char_uuid = uuid.UUID(character_id) + except: + flash("Character not found") + return redirect(url_for('lexicon.characters.characters', name=name)) + character: Optional[Character] = charq.try_from_public_id(g.db, char_uuid) + if not character: + flash("Character not found") + return redirect(url_for('lexicon.characters.characters', name=name)) + form = CharacterCreateForm() -# @bp.route('/', methods=['GET', 'POST']) -# @lexicon_param -# @player_required -# def characters(name): -# return render_template("characters.jinja") - # form = LexiconCharacterForm() - # cid = request.args.get('cid') - # if not cid: - # # No character specified, creating a new character - # return create_character(name, form) + if not form.is_submitted(): + # GET + form.name.data = character.name + form.signature.data = character.signature + return render_template('characters.edit.jinja', character=character, form=form) - # character = g.lexicon.cfg.character.get(cid) - # if not character: - # # Bad character id, abort - # flash('Character not found') - # return redirect(url_for('session.session', name=name)) - # if current_user.uid not in (character.player, g.lexicon.cfg.editor): - # # Only its owner and the editor can edit a character - # flash('Access denied') - # return redirect(url_for('session.session', name=name)) - # # Edit allowed - # return edit_character(name, form, character) + else: + # POST + if form.validate(): + # Data is valid + character.name = form.name.data + character.signature = form.signature.data + g.db.session.commit() + return redirect(url_for('lexicon.characters.characters', name=name)) - -# def edit_character(name, form, character): -# if not form.is_submitted(): -# # GET, populate with values -# return render_template( -# 'session.character.jinja', form=form.for_character(character)) - -# if not form.validate(): -# # POST with invalid data, return unchanged -# return render_template('session.character.jinja', form=form) - -# # POST with valid data, update character -# with g.lexicon.ctx.edit_config() as cfg: -# char = cfg.character[character.cid] -# char.name = form.characterName.data -# char.signature = form.defaultSignature.data -# flash('Character updated') -# return redirect(url_for('session.session', name=name)) + else: + # POST submitted invalid data + return render_template('characters.edit.jinja', character=character, form=form) # def create_character(name: str, form: LexiconCharacterForm): diff --git a/amanuensis/server/lexicon/characters/characters.edit.jinja b/amanuensis/server/lexicon/characters/characters.edit.jinja new file mode 100644 index 0000000..b9693d7 --- /dev/null +++ b/amanuensis/server/lexicon/characters/characters.edit.jinja @@ -0,0 +1,24 @@ +{% extends "lexicon.jinja" %} +{% block title %}Edit {{ character.name }} | {{ lexicon_title }}{% endblock %} + +{% block main %} +
+ {{ form.hidden_tag() }} +

+ {{ form.name.label }}
{{ form.name(size=32) }} +

+ {% for error in form.name.errors %} + {{ error }}
+ {% endfor %}

+

+ {{ form.signature.label }}
{{ form.signature(class_='fullwidth') }} +

+

{{ form.submit() }}

+
+ +{% for message in get_flashed_messages() %} +{{ message }}
+{% endfor %} + +{% endblock %} +{% set template_content_blocks = [self.main()] %} \ No newline at end of file diff --git a/amanuensis/server/lexicon/characters/characters.jinja b/amanuensis/server/lexicon/characters/characters.jinja index 72678b0..f3f023e 100644 --- a/amanuensis/server/lexicon/characters/characters.jinja +++ b/amanuensis/server/lexicon/characters/characters.jinja @@ -2,36 +2,23 @@ {% block title %}Character | {{ lexicon_title }}{% endblock %} {% block main %} -

Characters

{% set players = memq.get_players_in_lexicon(db, g.lexicon.id)|list %} {% set characters = charq.get_in_lexicon(db, g.lexicon.id)|list %}

This lexicon has {{ players|count }} player{% if players|count > 1 %}s{% endif %} and {{ characters|count }} character{% if characters|count > 1 %}s{% endif %}.

+{% for message in get_flashed_messages() %} +{{ message }}
+{% endfor %}
    {% for character in characters %}
  • -

    {{ character.name }}

    +

    {{ character.name }}

    Player: {{ character.user.username }}

    +{% if character.user == current_user %} +

    Edit this character

    +{% endif %}
  • {% endfor %}
-{#
- {{ form.hidden_tag() }} -

- {{ form.characterName.label }}
{{ form.characterName(size=32) }} -

- {% for error in form.characterName.errors %} - {{ error }}
- {% endfor %}

-

- {{ form.defaultSignature.label }}
{{ form.defaultSignature(class_='fullwidth') }} -

-

{{ form.submit() }}

-
#} - -{# {% for message in get_flashed_messages() %} -{{ message }}
-{% endfor %} #} - {% endblock %} {% set template_content_blocks = [self.main()] %} \ No newline at end of file diff --git a/amanuensis/server/lexicon/characters/forms.py b/amanuensis/server/lexicon/characters/forms.py new file mode 100644 index 0000000..a06edd8 --- /dev/null +++ b/amanuensis/server/lexicon/characters/forms.py @@ -0,0 +1,11 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, TextAreaField +from wtforms.validators import DataRequired + + +class CharacterCreateForm(FlaskForm): + """/lexicon//characters/edit/""" + + name = StringField("Character name", validators=[DataRequired()]) + signature = TextAreaField("Signature") + submit = SubmitField("Submit") -- 2.44.1 From 9f939fe57c34619edc7bc5c99abba22e4ae30b90 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 27 Aug 2021 07:50:48 -0700 Subject: [PATCH 6/9] Fix missing None check --- amanuensis/server/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amanuensis/server/helpers.py b/amanuensis/server/helpers.py index c68c190..434dcb3 100644 --- a/amanuensis/server/helpers.py +++ b/amanuensis/server/helpers.py @@ -88,7 +88,7 @@ def editor_required(route): user: User = current_user lexicon: Lexicon = g.lexicon mem: Optional[Membership] = memq.try_from_ids(db, user.id, lexicon.id) - if not mem.is_editor: + 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 route(*args, **kwargs) -- 2.44.1 From eec039c09a79f2e333dcdcbae4dbb3d88a9c303d Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 27 Aug 2021 08:19:29 -0700 Subject: [PATCH 7/9] Add character limit cli --- amanuensis/cli/lexicon.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/amanuensis/cli/lexicon.py b/amanuensis/cli/lexicon.py index 0c0e3c1..c3ea481 100644 --- a/amanuensis/cli/lexicon.py +++ b/amanuensis/cli/lexicon.py @@ -49,6 +49,7 @@ def command_create(args): @add_argument("--no-public", dest="public", action="store_const", const=False) @add_argument("--join", dest="join", action="store_const", const=True) @add_argument("--no-join", dest="join", action="store_const", const=False) +@add_argument("--char-limit", type=int, default=None) def command_edit(args): """ Update a lexicon's configuration. @@ -66,6 +67,9 @@ def command_edit(args): elif args.join == False: values["joinable"] = False + if args.char_limit: + values["character_limit"] = args.char_limit + result = db(update(Lexicon).where(Lexicon.name == args.name).values(**values)) LOG.info(f"Updated {result.rowcount} lexicons") db.session.commit() -- 2.44.1 From 45ee56d09b9cf6fa2db8de025c7fa31625998fb0 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 27 Aug 2021 11:53:37 -0700 Subject: [PATCH 8/9] Add character creation and signatures --- amanuensis/resources/page.css | 16 +++++-- amanuensis/server/lexicon.jinja | 2 +- .../server/lexicon/characters/__init__.py | 47 ++++++------------- .../lexicon/characters/characters.jinja | 10 ++++ 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index d32161f..536b854 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -95,9 +95,6 @@ div.contentblock { border-radius: 5px; word-break: break-word; } -div.contentblock h3 { - margin: 0.3em 0; -} a.phantom { color: #cc2200; } @@ -139,10 +136,21 @@ ul.blockitem-list li { border-inline-start: 3px solid black; padding-inline-start: 0.5em; } -ul.blockitem-list p { +ul.blockitem-list * { margin-block-start: 0.5em; margin-block-end: 0.5em; } +ul.blockitem-list pre { + background-color: lightgray; + padding-block-start: 2px; + padding-block-end: 2px; + padding-inline-start: 2px; + padding-inline-end: 2px; + border: 1px solid gray; + border-radius: 2px; + font-size: smaller; + white-space: break-spaces; +} div.dashboard-lexicon-unstarted { border-left-color: blue; } diff --git a/amanuensis/server/lexicon.jinja b/amanuensis/server/lexicon.jinja index 41ea53c..70862b4 100644 --- a/amanuensis/server/lexicon.jinja +++ b/amanuensis/server/lexicon.jinja @@ -9,7 +9,7 @@ {% block sb_logo %}{% endblock %} {% block sb_characters %}Characters{% endblock %} {% block sb_contents %}', methods=['GET', 'POST']) @@ -29,11 +30,11 @@ def edit(name, character_id): char_uuid = uuid.UUID(character_id) except: flash("Character not found") - return redirect(url_for('lexicon.characters.characters', name=name)) + return redirect(url_for('lexicon.characters.list', name=name)) character: Optional[Character] = charq.try_from_public_id(g.db, char_uuid) if not character: flash("Character not found") - return redirect(url_for('lexicon.characters.characters', name=name)) + return redirect(url_for('lexicon.characters.list', name=name)) form = CharacterCreateForm() @@ -50,38 +51,18 @@ def edit(name, character_id): character.name = form.name.data character.signature = form.signature.data g.db.session.commit() - return redirect(url_for('lexicon.characters.characters', name=name)) + return redirect(url_for('lexicon.characters.list', name=name)) else: # POST submitted invalid data return render_template('characters.edit.jinja', character=character, form=form) -# def create_character(name: str, form: LexiconCharacterForm): -# # Characters can't be created if the game has already started -# if g.lexicon.status != LexiconModel.PREGAME: -# flash("Characters can't be added after the game has started") -# return redirect(url_for('session.session', name=name)) -# # Characters can't be created beyond the per-player limit -# player_characters = get_player_characters(g.lexicon, current_user.uid) -# if len(list(player_characters)) >= g.lexicon.cfg.join.chars_per_player: -# flash("Can't create more characters") -# return redirect(url_for('session.session', name=name)) - -# if not form.is_submitted(): -# # GET, populate with default values -# return render_template( -# 'session.character.jinja', form=form.for_new()) - -# if not form.validate(): -# # POST with invalid data, return unchanged -# return render_template('session.character.jinja', form=form) - -# # POST with valid data, create character -# char_name = form.characterName.data -# cid = create_character_in_lexicon(current_user, g.lexicon, char_name) -# with g.lexicon.ctx.edit_config() as cfg: -# cfg.character[cid].signature = form.defaultSignature.data -# flash('Character created') -# return redirect(url_for('session.session', name=name)) - +@bp.get('/new/') +@lexicon_param +@player_required +def new(name): + dummy_name = f"{current_user.username}'s new character" + dummy_signature = "~" + charq.create(g.db, g.lexicon.id, current_user.id, dummy_name, dummy_signature) + return redirect(url_for('lexicon.characters.list', name=name)) diff --git a/amanuensis/server/lexicon/characters/characters.jinja b/amanuensis/server/lexicon/characters/characters.jinja index f3f023e..2d0a31c 100644 --- a/amanuensis/server/lexicon/characters/characters.jinja +++ b/amanuensis/server/lexicon/characters/characters.jinja @@ -1,4 +1,5 @@ {% extends "lexicon.jinja" %} +{% set current_page = "characters" %} {% block title %}Character | {{ lexicon_title }}{% endblock %} {% block main %} @@ -10,9 +11,18 @@ {{ message }}
{% endfor %}
    +{% if characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count < g.lexicon.character_limit %} +
  • +

    Create a new character

    +

    You have created {{ characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count }} out of {{ g.lexicon.character_limit }} allowed characters.

    +
  • +{% endif %} {% for character in characters %}
  • {{ character.name }}

    +{% if character.user == current_user %} +
    {{ character.signature }}
    +{% endif %}

    Player: {{ character.user.username }}

    {% if character.user == current_user %}

    Edit this character

    -- 2.44.1 From a9c97430de123f4d80c417fe52be7d916d0f7958 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 27 Aug 2021 12:22:01 -0700 Subject: [PATCH 9/9] Linting pass --- amanuensis/backend/character.py | 4 +- amanuensis/db/models.py | 1 + .../server/lexicon/characters/__init__.py | 30 +++++--- tests/test_character.py | 74 +++++++++++++++++++ 4 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 tests/test_character.py diff --git a/amanuensis/backend/character.py b/amanuensis/backend/character.py index c9be1fc..49e51a1 100644 --- a/amanuensis/backend/character.py +++ b/amanuensis/backend/character.py @@ -78,4 +78,6 @@ def get_in_lexicon(db: DbContext, lexicon_id: int) -> Sequence[Character]: def try_from_public_id(db: DbContext, public_id: UUID) -> Optional[Character]: """Get a character by its public id.""" - return db(select(Character).where(Character.public_id == public_id)).scalar_one_or_none() + return db( + select(Character).where(Character.public_id == public_id) + ).scalar_one_or_none() diff --git a/amanuensis/db/models.py b/amanuensis/db/models.py index 5f7a600..2e63c4c 100644 --- a/amanuensis/db/models.py +++ b/amanuensis/db/models.py @@ -30,6 +30,7 @@ class Uuid(TypeDecorator): """ impl = CHAR(32) + cache_ok = True def process_bind_param(self, value, dialect): if value is None: diff --git a/amanuensis/server/lexicon/characters/__init__.py b/amanuensis/server/lexicon/characters/__init__.py index 19773ed..c2b5c86 100644 --- a/amanuensis/server/lexicon/characters/__init__.py +++ b/amanuensis/server/lexicon/characters/__init__.py @@ -2,7 +2,7 @@ from typing import Optional import uuid from flask import Blueprint, render_template, url_for, g, flash -from flask_login import current_user +from flask_login import current_user from werkzeug.utils import redirect from amanuensis.backend import charq @@ -15,14 +15,14 @@ from .forms import CharacterCreateForm bp = Blueprint("characters", __name__, url_prefix="/characters", template_folder=".") -@bp.get('/') +@bp.get("/") @lexicon_param @player_required def list(name): - return render_template('characters.jinja', name=name) + return render_template("characters.jinja", name=name) -@bp.route('/edit/', methods=['GET', 'POST']) +@bp.route("/edit/", methods=["GET", "POST"]) @lexicon_param @player_required def edit(name, character_id): @@ -30,11 +30,11 @@ def edit(name, character_id): char_uuid = uuid.UUID(character_id) except: flash("Character not found") - return redirect(url_for('lexicon.characters.list', name=name)) + return redirect(url_for("lexicon.characters.list", name=name)) character: Optional[Character] = charq.try_from_public_id(g.db, char_uuid) if not character: flash("Character not found") - return redirect(url_for('lexicon.characters.list', name=name)) + return redirect(url_for("lexicon.characters.list", name=name)) form = CharacterCreateForm() @@ -42,7 +42,7 @@ def edit(name, character_id): # 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", character=character, form=form) else: # POST @@ -51,18 +51,24 @@ def edit(name, character_id): 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", name=name)) else: # POST submitted invalid data - return render_template('characters.edit.jinja', character=character, form=form) + return render_template( + "characters.edit.jinja", character=character, form=form + ) -@bp.get('/new/') +@bp.get("/new/") @lexicon_param @player_required def new(name): dummy_name = f"{current_user.username}'s new character" dummy_signature = "~" - charq.create(g.db, g.lexicon.id, current_user.id, dummy_name, dummy_signature) - return redirect(url_for('lexicon.characters.list', name=name)) + 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) + ) diff --git a/tests/test_character.py b/tests/test_character.py new file mode 100644 index 0000000..ccd5b51 --- /dev/null +++ b/tests/test_character.py @@ -0,0 +1,74 @@ +import os +from urllib.parse import urlsplit + +from bs4 import BeautifulSoup +from flask import Flask, url_for + +from amanuensis.backend import memq, charq +from amanuensis.db import DbContext + +from tests.conftest import ObjectFactory + + +def test_character_view(db: DbContext, app: Flask, make: ObjectFactory): + """Test the lexicon character list, create, and edit pages.""" + 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 + user = make.user(username=username, password=username) + assert user + user_client = make.client(user.id) + assert client + user_client.login(client) + + # Create a lexicon and join + lexicon = make.lexicon() + assert lexicon + mem = memq.create(db, user.id, lexicon.id, is_editor=False) + assert mem + + # The character page exists + list_url = url_for("lexicon.characters.list", 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) + assert new_url.encode("utf8") in response.data + + # The character creation endpoint works + response = client.get(new_url) + assert response.status_code == 302 + chars = list(charq.get_in_lexicon(db, lexicon.id)) + assert len(chars) == 1 + assert chars[0].user_id == user.id + created_redirect = response.location + assert str(chars[0].public_id) in created_redirect + + # The character edit page works + response = client.get(created_redirect) + assert chars[0].name.encode("utf8") in response.data + assert chars[0].signature.encode("utf8") in response.data + assert b"csrf_token" in response.data + + # Submitting the edit page works + soup = BeautifulSoup(response.data, features="html.parser") + csrf_token = soup.find(id="csrf_token")["value"] + assert csrf_token + response = client.post( + 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 + chars = list(charq.get_in_lexicon(db, lexicon.id)) + assert len(chars) == 1 + assert chars[0].user_id == user.id + assert chars[0].name == charname + assert chars[0].signature == char_sig -- 2.44.1