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 %}.
+
+{#
+
+ {{ 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
-
-
-{% 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 %}
+
+
+{% 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 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 %}
+-
+
+
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