diff --git a/amanuensis/backend/character.py b/amanuensis/backend/character.py index 01f5804..49e51a1 100644 --- a/amanuensis/backend/character.py +++ b/amanuensis/backend/character.py @@ -2,7 +2,8 @@ Character query interface """ -from typing import Optional +from typing import Optional, Sequence +from uuid import UUID from sqlalchemy import select, func @@ -68,3 +69,15 @@ 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() + + +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/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/cli/__init__.py b/amanuensis/cli/__init__.py index eb9e111..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 @@ -50,7 +51,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 +98,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") @@ -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 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() 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/resources/page.css b/amanuensis/resources/page.css index b22db7b..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; } @@ -126,6 +123,34 @@ 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; +} +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/__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/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) diff --git a/amanuensis/server/lexicon.jinja b/amanuensis/server/lexicon.jinja index 9c976eb..70862b4 100644 --- a/amanuensis/server/lexicon.jinja +++ b/amanuensis/server/lexicon.jinja @@ -7,8 +7,10 @@ {% endblock %} {% block sb_logo %}{% endblock %} -{% block sb_home %}Home -{% endblock %} +{% block sb_characters %}Characters{% endblock %} {% block sb_contents %}", methods=["GET", "POST"]) +@lexicon_param +@player_required +def edit(name, character_id): + try: + char_uuid = uuid.UUID(character_id) + except: + flash("Character not found") + 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)) + + form = CharacterCreateForm() + + 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) + + 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.list", name=name)) + + else: + # POST submitted invalid data + return render_template( + "characters.edit.jinja", character=character, form=form + ) + + +@bp.get("/new/") +@lexicon_param +@player_required +def new(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) + ) 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.signature.label }}
{{ form.signature(class_='fullwidth') }}
+
{{ form.submit() }}
+ + +{% for message in get_flashed_messages() %} +{{ message }}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 }}You have created {{ characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count }} out of {{ g.lexicon.character_limit }} allowed characters.
+{{ character.signature }}+{% endif %} +
Player: {{ character.user.username }}
+{% if character.user == current_user %} + +{% endif %} +