Add character page and create/edit workflows #18
|
@ -2,7 +2,8 @@
|
||||||
Character query interface
|
Character query interface
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional, Sequence
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy import select, func
|
from sqlalchemy import select, func
|
||||||
|
|
||||||
|
@ -68,3 +69,15 @@ def create(
|
||||||
db.session.add(new_character)
|
db.session.add(new_character)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return new_character
|
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()
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
Membership query interface
|
Membership query interface
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
from sqlalchemy import select, func
|
from sqlalchemy import select, func
|
||||||
|
|
||||||
from amanuensis.db import DbContext, Membership
|
from amanuensis.db import DbContext, Membership
|
||||||
|
@ -66,6 +68,11 @@ def create(
|
||||||
return new_membership
|
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:
|
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."""
|
"""Get a membership by the user and lexicon ids, or None if no such membership was found."""
|
||||||
return db(
|
return db(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import os
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
import amanuensis.cli.admin
|
import amanuensis.cli.admin
|
||||||
|
import amanuensis.cli.character
|
||||||
import amanuensis.cli.lexicon
|
import amanuensis.cli.lexicon
|
||||||
import amanuensis.cli.user
|
import amanuensis.cli.user
|
||||||
from amanuensis.db import DbContext
|
from amanuensis.db import DbContext
|
||||||
|
@ -50,7 +51,7 @@ def add_subcommand(subparsers, module) -> None:
|
||||||
command_parser: ArgumentParser = subparsers.add_parser(
|
command_parser: ArgumentParser = subparsers.add_parser(
|
||||||
command_name, help=command_help
|
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
|
# Add all subcommands in the command module
|
||||||
subcommands = command_parser.add_subparsers(metavar="SUBCOMMAND")
|
subcommands = command_parser.add_subparsers(metavar="SUBCOMMAND")
|
||||||
|
@ -97,7 +98,7 @@ def main():
|
||||||
parser = ArgumentParser()
|
parser = ArgumentParser()
|
||||||
parser.set_defaults(
|
parser.set_defaults(
|
||||||
parser=parser,
|
parser=parser,
|
||||||
func=lambda args: parser.print_usage(),
|
func=lambda args: parser.print_help(),
|
||||||
get_db=None,
|
get_db=None,
|
||||||
)
|
)
|
||||||
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
|
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
|
||||||
|
@ -108,6 +109,7 @@ def main():
|
||||||
# Add commands from cli submodules
|
# Add commands from cli submodules
|
||||||
subparsers = parser.add_subparsers(metavar="COMMAND")
|
subparsers = parser.add_subparsers(metavar="COMMAND")
|
||||||
add_subcommand(subparsers, amanuensis.cli.admin)
|
add_subcommand(subparsers, amanuensis.cli.admin)
|
||||||
|
add_subcommand(subparsers, amanuensis.cli.character)
|
||||||
add_subcommand(subparsers, amanuensis.cli.lexicon)
|
add_subcommand(subparsers, amanuensis.cli.lexicon)
|
||||||
add_subcommand(subparsers, amanuensis.cli.user)
|
add_subcommand(subparsers, amanuensis.cli.user)
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -49,6 +49,7 @@ def command_create(args):
|
||||||
@add_argument("--no-public", dest="public", action="store_const", const=False)
|
@add_argument("--no-public", dest="public", action="store_const", const=False)
|
||||||
@add_argument("--join", dest="join", action="store_const", const=True)
|
@add_argument("--join", dest="join", action="store_const", const=True)
|
||||||
@add_argument("--no-join", dest="join", action="store_const", const=False)
|
@add_argument("--no-join", dest="join", action="store_const", const=False)
|
||||||
|
@add_argument("--char-limit", type=int, default=None)
|
||||||
def command_edit(args):
|
def command_edit(args):
|
||||||
"""
|
"""
|
||||||
Update a lexicon's configuration.
|
Update a lexicon's configuration.
|
||||||
|
@ -66,6 +67,9 @@ def command_edit(args):
|
||||||
elif args.join == False:
|
elif args.join == False:
|
||||||
values["joinable"] = 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))
|
result = db(update(Lexicon).where(Lexicon.name == args.name).values(**values))
|
||||||
LOG.info(f"Updated {result.rowcount} lexicons")
|
LOG.info(f"Updated {result.rowcount} lexicons")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
|
@ -30,6 +30,7 @@ class Uuid(TypeDecorator):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
impl = CHAR(32)
|
impl = CHAR(32)
|
||||||
|
cache_ok = True
|
||||||
|
|
||||||
def process_bind_param(self, value, dialect):
|
def process_bind_param(self, value, dialect):
|
||||||
if value is None:
|
if value is None:
|
||||||
|
|
|
@ -95,9 +95,6 @@ div.contentblock {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
div.contentblock h3 {
|
|
||||||
margin: 0.3em 0;
|
|
||||||
}
|
|
||||||
a.phantom {
|
a.phantom {
|
||||||
color: #cc2200;
|
color: #cc2200;
|
||||||
}
|
}
|
||||||
|
@ -126,6 +123,34 @@ div.dashboard-lexicon-item {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border-left: 3px solid black;
|
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 {
|
div.dashboard-lexicon-unstarted {
|
||||||
border-left-color: blue;
|
border-left-color: blue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import os
|
||||||
|
|
||||||
from flask import Flask, g, url_for, redirect
|
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.config import AmanuensisConfig, CommandLineConfig
|
||||||
from amanuensis.db import DbContext
|
from amanuensis.db import DbContext
|
||||||
from amanuensis.parser import filesafe_title
|
from amanuensis.parser import filesafe_title
|
||||||
|
@ -68,7 +68,7 @@ def get_app(
|
||||||
|
|
||||||
# Configure jinja options
|
# Configure jinja options
|
||||||
def include_backend():
|
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.jinja_options.update(trim_blocks=True, lstrip_blocks=True)
|
||||||
app.template_filter("date")(date_format)
|
app.template_filter("date")(date_format)
|
||||||
|
|
|
@ -88,7 +88,7 @@ def editor_required(route):
|
||||||
user: User = current_user
|
user: User = current_user
|
||||||
lexicon: Lexicon = g.lexicon
|
lexicon: Lexicon = g.lexicon
|
||||||
mem: Optional[Membership] = memq.try_from_ids(db, user.id, lexicon.id)
|
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")
|
flash("You must be the editor to view this page")
|
||||||
return redirect(url_for('lexicon.contents', name=lexicon.name))
|
return redirect(url_for('lexicon.contents', name=lexicon.name))
|
||||||
return route(*args, **kwargs)
|
return route(*args, **kwargs)
|
||||||
|
|
|
@ -7,8 +7,10 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block sb_logo %}{% endblock %}
|
{% block sb_logo %}{% endblock %}
|
||||||
{% block sb_home %}<a href="{{ url_for('home.home') }}">Home</a>
|
{% block sb_characters %}<a
|
||||||
{% endblock %}
|
{% if current_page == "characters" %}class="current-page"
|
||||||
|
{% else %}href="{{ url_for('lexicon.characters.list', name=g.lexicon.name) }}"
|
||||||
|
{% endif %}>Characters</a>{% endblock %}
|
||||||
{% block sb_contents %}<a
|
{% block sb_contents %}<a
|
||||||
{% if current_page == "contents" %}class="current-page"
|
{% if current_page == "contents" %}class="current-page"
|
||||||
{% else %}href="{{ url_for('lexicon.contents', name=g.lexicon.name) }}"
|
{% else %}href="{{ url_for('lexicon.contents', name=g.lexicon.name) }}"
|
||||||
|
@ -32,7 +34,7 @@
|
||||||
) %}
|
) %}
|
||||||
{# self.sb_logo(), #}
|
{# self.sb_logo(), #}
|
||||||
{% set template_sidebar_rows = [
|
{% set template_sidebar_rows = [
|
||||||
self.sb_home(),
|
self.sb_characters(),
|
||||||
self.sb_contents(),
|
self.sb_contents(),
|
||||||
self.sb_rules(),
|
self.sb_rules(),
|
||||||
self.sb_session(),
|
self.sb_session(),
|
||||||
|
@ -40,7 +42,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{# self.sb_logo(), #}
|
{# self.sb_logo(), #}
|
||||||
{% set template_sidebar_rows = [
|
{% set template_sidebar_rows = [
|
||||||
self.sb_home(),
|
self.sb_characters(),
|
||||||
self.sb_contents(),
|
self.sb_contents(),
|
||||||
self.sb_rules(),
|
self.sb_rules(),
|
||||||
self.sb_stats()] %}
|
self.sb_stats()] %}
|
||||||
|
|
|
@ -6,10 +6,12 @@ from amanuensis.db import DbContext, Lexicon, User
|
||||||
from amanuensis.errors import ArgumentError
|
from amanuensis.errors import ArgumentError
|
||||||
from amanuensis.server.helpers import lexicon_param, player_required_if_not_public
|
from amanuensis.server.helpers import lexicon_param, player_required_if_not_public
|
||||||
|
|
||||||
|
from .characters import bp as characters_bp
|
||||||
from .forms import LexiconJoinForm
|
from .forms import LexiconJoinForm
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("lexicon", __name__, url_prefix="/lexicon/<name>", template_folder=".")
|
bp = Blueprint("lexicon", __name__, url_prefix="/lexicon/<name>", template_folder=".")
|
||||||
|
bp.register_blueprint(characters_bp)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/join/", methods=["GET", "POST"])
|
@bp.route("/join/", methods=["GET", "POST"])
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
from typing import Optional
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from flask import Blueprint, render_template, url_for, g, flash
|
||||||
|
from flask_login import current_user
|
||||||
|
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 list(name):
|
||||||
|
return render_template("characters.jinja", name=name)
|
||||||
|
|
||||||
|
|
||||||
|
@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.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)
|
||||||
|
)
|
|
@ -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()] %}
|
|
@ -0,0 +1,34 @@
|
||||||
|
{% extends "lexicon.jinja" %}
|
||||||
|
{% set current_page = "characters" %}
|
||||||
|
{% 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">
|
||||||
|
{% if characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count < g.lexicon.character_limit %}
|
||||||
|
<li>
|
||||||
|
<h3><a href="{{ url_for('lexicon.characters.new', name=name) }}">Create a new character</a></h3>
|
||||||
|
<p>You have created {{ characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count }} out of {{ g.lexicon.character_limit }} allowed characters.</p>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for character in characters %}
|
||||||
|
<li>
|
||||||
|
<h3>{{ character.name }}</h3>
|
||||||
|
{% if character.user == current_user %}
|
||||||
|
<pre>{{ character.signature }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
<p>Player: {{ character.user.username }}</p>
|
||||||
|
{% if character.user == current_user %}
|
||||||
|
<p><a href="{{ url_for('lexicon.characters.edit', name=g.lexicon.name, character_id=character.public_id) }}">Edit this character</a></p>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
{% set template_content_blocks = [self.main()] %}
|
|
@ -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")
|
|
@ -13,8 +13,13 @@
|
||||||
<div id="login-status" {% block login_status_attr %}{% endblock %}>
|
<div id="login-status" {% block login_status_attr %}{% endblock %}>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<b>{{ current_user.username -}}</b>
|
<b>{{ current_user.username -}}</b>
|
||||||
(<a href="{{ url_for('auth.logout') }}">Logout</a>)
|
‧
|
||||||
|
<a href="{{ url_for('home.home') }}">Home</a>
|
||||||
|
‧
|
||||||
|
<a href="{{ url_for('auth.logout') }}">Logout</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<a href="{{ url_for('home.home') }}">Home</a>
|
||||||
|
‧
|
||||||
<a href="{{ url_for('auth.login') }}">Login</a>
|
<a href="{{ url_for('auth.login') }}">Login</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -68,77 +68,6 @@ def session(name):
|
||||||
publish_form=form)
|
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'])
|
@bp_session.route('/settings/', methods=['GET', 'POST'])
|
||||||
@lexicon_param
|
@lexicon_param
|
||||||
@editor_required
|
@editor_required
|
||||||
|
|
|
@ -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()] %}
|
|
|
@ -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
|
Loading…
Reference in New Issue