Compare commits

...

6 Commits

16 changed files with 234 additions and 108 deletions

View File

@ -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,13 @@ 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()

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -126,6 +126,23 @@ 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 p {
margin-block-start: 0.5em;
margin-block-end: 0.5em;
}
div.dashboard-lexicon-unstarted {
border-left-color: blue;
}

View File

@ -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)

View File

@ -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)

View File

@ -7,8 +7,10 @@
{% endblock %}
{% block sb_logo %}{% endblock %}
{% block sb_home %}<a href="{{ url_for('home.home') }}">Home</a>
{% endblock %}
{% block sb_characters %}<a
{% if current_page == "characters" %}class="current-page"
{% else %}href="{{ url_for('lexicon.characters.characters', name=g.lexicon.name) }}"
{% endif %}>Characters</a>{% endblock %}
{% block sb_contents %}<a
{% if current_page == "contents" %}class="current-page"
{% else %}href="{{ url_for('lexicon.contents', name=g.lexicon.name) }}"
@ -32,7 +34,7 @@
) %}
{# self.sb_logo(), #}
{% set template_sidebar_rows = [
self.sb_home(),
self.sb_characters(),
self.sb_contents(),
self.sb_rules(),
self.sb_session(),
@ -40,7 +42,7 @@
{% else %}
{# self.sb_logo(), #}
{% set template_sidebar_rows = [
self.sb_home(),
self.sb_characters(),
self.sb_contents(),
self.sb_rules(),
self.sb_stats()] %}

View File

@ -6,10 +6,12 @@ from amanuensis.db import DbContext, Lexicon, User
from amanuensis.errors import ArgumentError
from amanuensis.server.helpers import lexicon_param, player_required_if_not_public
from .characters import bp as characters_bp
from .forms import LexiconJoinForm
bp = Blueprint("lexicon", __name__, url_prefix="/lexicon/<name>", template_folder=".")
bp.register_blueprint(characters_bp)
@bp.route("/join/", methods=["GET", "POST"])

View File

@ -0,0 +1,87 @@
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=".")
@bp.get('/')
@lexicon_param
@player_required
def characters(name):
return render_template('characters.jinja')
@bp.route('/edit/<character_id>', 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.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()
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.characters', 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))

View File

@ -0,0 +1,24 @@
{% extends "lexicon.jinja" %}
{% block title %}Edit {{ character.name }} | {{ lexicon_title }}{% endblock %}
{% block main %}
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.name.label }}<br>{{ form.name(size=32) }}
</p>
{% for error in form.name.errors %}
<span style="color: #ff0000">{{ error }}</span><br>
{% endfor %}</p>
<p>
{{ form.signature.label }}<br>{{ form.signature(class_='fullwidth') }}
</p>
<p>{{ form.submit() }}</p>
</form>
{% for message in get_flashed_messages() %}
<span style="color:#ff0000">{{ message }}</span><br>
{% endfor %}
{% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -0,0 +1,24 @@
{% extends "lexicon.jinja" %}
{% block title %}Character | {{ lexicon_title }}{% endblock %}
{% block main %}
<h1>Characters</h1>
{% set players = memq.get_players_in_lexicon(db, g.lexicon.id)|list %}
{% set characters = charq.get_in_lexicon(db, g.lexicon.id)|list %}
<p>This lexicon has <b>{{ players|count }}</b> player{% if players|count > 1 %}s{% endif %} and <b>{{ characters|count }}</b> character{% if characters|count > 1 %}s{% endif %}.</p>
{% for message in get_flashed_messages() %}
<span style="color:#ff0000">{{ message }}</span><br>
{% endfor %}
<ul class="blockitem-list">
{% for character in characters %}
<li>
<h3>{{ character.name }}</h3>
<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>
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -0,0 +1,11 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired
class CharacterCreateForm(FlaskForm):
"""/lexicon/<name>/characters/edit/<character_id>"""
name = StringField("Character name", validators=[DataRequired()])
signature = TextAreaField("Signature")
submit = SubmitField("Submit")

View File

@ -13,8 +13,13 @@
<div id="login-status" {% block login_status_attr %}{% endblock %}>
{% if current_user.is_authenticated %}
<b>{{ current_user.username -}}</b>
(<a href="{{ url_for('auth.logout') }}">Logout</a>)
&#8231;
<a href="{{ url_for('home.home') }}">Home</a>
&#8231;
<a href="{{ url_for('auth.logout') }}">Logout</a>
{% else %}
<a href="{{ url_for('home.home') }}">Home</a>
&#8231;
<a href="{{ url_for('auth.login') }}">Login</a>
{% endif %}
</div>

View File

@ -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

View File

@ -1,26 +0,0 @@
{% extends "lexicon.jinja" %}
{% block title %}Character | {{ lexicon_title }}{% endblock %}
{% block main %}
<h1>Character</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.characterName.label }}<br>{{ form.characterName(size=32) }}
</p>
{% for error in form.characterName.errors %}
<span style="color: #ff0000">{{ error }}</span><br>
{% endfor %}</p>
<p>
{{ form.defaultSignature.label }}<br>{{ form.defaultSignature(class_='fullwidth') }}
</p>
<p>{{ form.submit() }}</p>
</form>
{% for message in get_flashed_messages() %}
<span style="color:#ff0000">{{ message }}</span><br>
{% endfor %}
{% endblock %}
{% set template_content_blocks = [self.main()] %}