Compare commits

...

6 Commits

Author SHA1 Message Date
Tim Van Baak 7e54b14b7f Implement player-specific and game setup settings 2021-09-04 15:34:18 -07:00
Tim Van Baak ba722562a8 Add settings page skeleton 2021-09-04 13:37:28 -07:00
Tim Van Baak c5e68f1999 Refactor some css colors
These will be reused in the settings tabs and perhaps elsewhere
2021-09-04 13:24:11 -07:00
Tim Van Baak 30c32f9860 fixup for lexicon_name 2021-09-04 10:08:04 -07:00
Tim Van Baak 7a082e1da7 Add local proxies for the current lexicon and membership
This somewhat duplicates loading the objects into flask.g, but doesn't require
the lexicon route decorators. Additionally, lazy-loading these objects can save
the occasional db call, and for the current membership this is more convenient
than having to add a second decorator or staple it to the lexicon loading.
2021-09-04 09:58:58 -07:00
Tim Van Baak bb49e319dd Rename route parameter to lexicon_name
This disambiguates and future-proofs the route parameters so they can be more
easily referenced in before_request contexts shared by all routes.
2021-09-04 09:53:06 -07:00
13 changed files with 461 additions and 66 deletions

View File

@ -84,7 +84,11 @@ def password_check(db: DbContext, lexicon_id: int, password: str) -> bool:
def password_set(db: DbContext, lexicon_id: int, new_password: Optional[str]) -> None: def password_set(db: DbContext, lexicon_id: int, new_password: Optional[str]) -> None:
"""Set or clear a lexicon's password.""" """Set or clear a lexicon's password."""
password_hash = generate_password_hash(new_password) if new_password else None password_hash = generate_password_hash(new_password) if new_password else None
db(update(Lexicon).where(Lexicon.id == lexicon_id).values(password=password_hash)) db(
update(Lexicon)
.where(Lexicon.id == lexicon_id)
.values(join_password=password_hash)
)
db.session.commit() db.session.commit()

View File

@ -1,3 +1,8 @@
:root {
--button-default: #dddddd;
--button-hover: #cccccc;
--button-current: #bbbbbb;
}
body { body {
background-color: #eeeeee; background-color: #eeeeee;
line-height: 1.4; line-height: 1.4;
@ -57,7 +62,7 @@ div.misclinks table td a {
table a { table a {
display: flex; display: flex;
padding: 3px; padding: 3px;
background-color: #dddddd; background-color: var(--button-default);
border-radius: 5px; border-radius: 5px;
text-decoration: none; text-decoration: none;
} }
@ -65,13 +70,13 @@ div#sidebar table a {
justify-content: center; justify-content: center;
} }
table a:hover { table a:hover {
background-color: #cccccc; background-color: var(--button-hover);
} }
div#sidebar table a.current-page { div#sidebar table a.current-page {
background-color: #bbbbbb; background-color: var(--button-current);
} }
div#sidebar table a.current-page:hover { div#sidebar table a.current-page:hover {
background-color: #bbbbbb; background-color: var(--button-current);
} }
div#sidebar table td { div#sidebar table td {
padding: 0px; margin: 3px 0; padding: 0px; margin: 3px 0;
@ -109,6 +114,10 @@ textarea.fullwidth {
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
input.fullwidth {
width: 100%;
box-sizing: border-box;
}
input.smallnumber { input.smallnumber {
width: 4em; width: 4em;
} }
@ -168,6 +177,36 @@ div.dashboard-lexicon-item p {
margin-block-start: 0.5em; margin-block-start: 0.5em;
margin-block-end: 0.5em; margin-block-end: 0.5em;
} }
ul.unordered-tabs {
list-style: none;
margin-block-start: 0;
margin-block-end: 0;
margin-inline-start: 0;
margin-inline-end: 0;
padding-block-start: 0;
padding-block-end: 0;
padding-inline-start: 0;
padding-inline-end: 0;
}
ul.unordered-tabs li {
display: inline-block;
margin: 3px;
}
ul.unordered-tabs li a {
background-color: var(--button-current);
display: flex;
border: 5px solid var(--button-current);
border-radius: 5px;
text-decoration: none;
}
ul.unordered-tabs li a[href] {
background-color: var(--button-default);
border-color: var(--button-default);
}
ul.unordered-tabs li a[href]:hover {
background-color: var(--button-hover);
border-color: var(--button-hover);
}
@media only screen and (max-width: 816px) { @media only screen and (max-width: 816px) {
div#wrapper { div#wrapper {
padding: 5px; padding: 5px;

View File

@ -9,7 +9,7 @@ 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
import amanuensis.server.auth as auth import amanuensis.server.auth as auth
from amanuensis.server.helpers import UuidConverter from amanuensis.server.helpers import UuidConverter, current_lexicon, current_membership
import amanuensis.server.home as home import amanuensis.server.home as home
import amanuensis.server.lexicon as lexicon import amanuensis.server.lexicon as lexicon
@ -27,7 +27,7 @@ def article_link(title):
"""Get the url for a lexicon by its title""" """Get the url for a lexicon by its title"""
return url_for( return url_for(
'lexicon.article', 'lexicon.article',
name=g.lexicon.name, lexicon_name=g.lexicon.name,
title=filesafe_title(title)) title=filesafe_title(title))
@ -68,13 +68,21 @@ def get_app(
app.teardown_appcontext(db_teardown) app.teardown_appcontext(db_teardown)
# Configure jinja options # Configure jinja options
def include_backend(): def add_jinja_context():
return {"db": db, "lexiq": lexiq, "userq": userq, "memq": memq, "charq": charq} return {
"db": db,
"lexiq": lexiq,
"userq": userq,
"memq": memq,
"charq": charq,
"current_lexicon": current_lexicon,
"current_membership": current_membership
}
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)
app.template_filter("articlelink")(article_link) app.template_filter("articlelink")(article_link)
app.context_processor(include_backend) app.context_processor(add_jinja_context)
# Set up uuid route converter # Set up uuid route converter
app.url_map.converters["uuid"] = UuidConverter app.url_map.converters["uuid"] = UuidConverter

View File

@ -2,8 +2,17 @@ from functools import wraps
from typing import Optional, Any from typing import Optional, Any
from uuid import UUID from uuid import UUID
from flask import g, flash, redirect, url_for from flask import (
_request_ctx_stack,
flash,
g,
has_request_context,
redirect,
request,
url_for,
)
from flask_login import current_user from flask_login import current_user
from werkzeug.local import LocalProxy
from werkzeug.routing import BaseConverter, ValidationError from werkzeug.routing import BaseConverter, ValidationError
from amanuensis.backend import lexiq, memq from amanuensis.backend import lexiq, memq
@ -26,6 +35,45 @@ class UuidConverter(BaseConverter):
return str(value) return str(value)
def get_current_lexicon():
# Check if the request context is for a lexicon page
if not has_request_context():
return None
lexicon_name = request.view_args.get("lexicon_name")
if not lexicon_name:
return None
# Pull up the lexicon if it exists and cache it in the request context
if not hasattr(_request_ctx_stack.top, "lexicon"):
db: DbContext = g.db
lexicon: Optional[Lexicon] = lexiq.try_from_name(db, lexicon_name)
setattr(_request_ctx_stack.top, "lexicon", lexicon)
# Return the cached lexicon
return getattr(_request_ctx_stack.top, "lexicon", None)
current_lexicon = LocalProxy(get_current_lexicon)
def get_current_membership():
# Base the current membership on the current user and the current lexicon
user: User = current_user
if not user or not user.is_authenticated:
return None
lexicon: Lexicon = current_lexicon
if not lexicon:
return None
# Pull up the membership and cache it in the request context
if not hasattr(_request_ctx_stack.top, "membership"):
db: DbContext = g.db
mem: Membership = memq.try_from_ids(db, user.id, lexicon.id)
setattr(_request_ctx_stack.top, "membership", mem)
# Return cached membership
return getattr(_request_ctx_stack.top, "membership", None)
current_membership = LocalProxy(get_current_membership)
def lexicon_param(route): def lexicon_param(route):
""" """
Wrapper for loading a route's lexicon to `g`. Wrapper for loading a route's lexicon to `g`.
@ -34,7 +82,7 @@ def lexicon_param(route):
@wraps(route) @wraps(route)
def with_lexicon(*args, **kwargs): def with_lexicon(*args, **kwargs):
db: DbContext = g.db db: DbContext = g.db
name: str = kwargs.get('name') name: str = kwargs.get('lexicon_name')
lexicon: Optional[Lexicon] = lexiq.try_from_name(db, name) lexicon: Optional[Lexicon] = lexiq.try_from_name(db, name)
if lexicon is None: if lexicon is None:
flash(f"Couldn't find a lexicon with the name \"{name}\"") flash(f"Couldn't find a lexicon with the name \"{name}\"")
@ -71,7 +119,7 @@ def player_required(route):
if not mem: if not mem:
flash("You must be a player to view this page") flash("You must be a player to view this page")
if lexicon.public: if lexicon.public:
return redirect(url_for('lexicon.contents', name=lexicon.name)) return redirect(url_for('lexicon.contents', lexicon_name=lexicon.name))
else: else:
return redirect(url_for('home.home')) return redirect(url_for('home.home'))
return route(*args, **kwargs) return route(*args, **kwargs)
@ -108,6 +156,6 @@ def editor_required(route):
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 or 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', lexicon_name=lexicon.name))
return route(*args, **kwargs) return route(*args, **kwargs)
return editor_route return editor_route

View File

@ -9,41 +9,28 @@
{% block sb_logo %}{% endblock %} {% block sb_logo %}{% endblock %}
{% block sb_characters %}<a {% block sb_characters %}<a
{% if current_page == "characters" %}class="current-page" {% if current_page == "characters" %}class="current-page"
{% else %}href="{{ url_for('lexicon.characters.list', name=g.lexicon.name) }}" {% else %}href="{{ url_for('lexicon.characters.list', lexicon_name=g.lexicon.name) }}"
{% endif %}>Characters</a>{% endblock %} {% 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', lexicon_name=g.lexicon.name) }}"
{% endif %}>Contents</a>{% endblock %} {% endif %}>Contents</a>{% endblock %}
{% block sb_rules %}<a {% block sb_rules %}<a
{% if current_page == "rules" %}class="current-page" {% if current_page == "rules" %}class="current-page"
{% else %}href="{{ url_for('lexicon.rules', name=g.lexicon.name) }}" {% else %}href="{{ url_for('lexicon.rules', lexicon_name=g.lexicon.name) }}"
{% endif %}>Rules</a>{% endblock %} {% endif %}>Rules</a>{% endblock %}
{% block sb_session %}<a {% block sb_settings %}<a
{% if current_page == "session" %}class="current-page" {% if current_page == "settings" %}class="current-page"
{% else %}href="#{#{ url_for('session.session', name=g.lexicon.name) }#}" {% else %}href="{{ url_for('lexicon.settings.page', lexicon_name=g.lexicon.name) }}"
{% endif %}>Session</a>{% endblock %} {% endif %}>Settings</a>{% endblock %}
{% block sb_stats %}<a {% block sb_stats %}<a
{% if current_page == "statistics" %}class="current-page" {% if current_page == "statistics" %}class="current-page"
{% else %}href="{{ url_for('lexicon.stats', name=g.lexicon.name) }}" {% else %}href="{{ url_for('lexicon.stats', lexicon_name=g.lexicon.name) }}"
{% endif %}>Statistics</a>{% endblock %} {% endif %}>Statistics</a>{% endblock %}
{% if current_user.is_authenticated and (
current_user.is_site_admin
or memq.try_from_ids(g.db, current_user.id, g.lexicon.id)
) %}
{# self.sb_logo(), #}
{% set template_sidebar_rows = [ {% set template_sidebar_rows = [
self.sb_characters(), self.sb_characters(),
self.sb_contents(), self.sb_contents(),
self.sb_rules(), self.sb_rules(),
self.sb_session(), self.sb_settings(),
self.sb_stats()] %} self.sb_stats()] %}
{% else %}
{# self.sb_logo(), #}
{% set template_sidebar_rows = [
self.sb_characters(),
self.sb_contents(),
self.sb_rules(),
self.sb_stats()] %}
{% endif %}

View File

@ -8,16 +8,20 @@ from amanuensis.server.helpers import lexicon_param, player_required_if_not_publ
from .characters import bp as characters_bp from .characters import bp as characters_bp
from .forms import LexiconJoinForm from .forms import LexiconJoinForm
from .settings import bp as settings_bp
bp = Blueprint("lexicon", __name__, url_prefix="/lexicon/<name>", template_folder=".") bp = Blueprint(
"lexicon", __name__, url_prefix="/lexicon/<lexicon_name>", template_folder="."
)
bp.register_blueprint(characters_bp) bp.register_blueprint(characters_bp)
bp.register_blueprint(settings_bp)
@bp.route("/join/", methods=["GET", "POST"]) @bp.route("/join/", methods=["GET", "POST"])
@lexicon_param @lexicon_param
@login_required @login_required
def join(name): def join(lexicon_name):
lexicon: Lexicon = g.lexicon lexicon: Lexicon = g.lexicon
if not lexicon.joinable: if not lexicon.joinable:
flash("This game isn't open for joining") flash("This game isn't open for joining")
@ -27,7 +31,9 @@ def join(name):
if not form.validate_on_submit(): if not form.validate_on_submit():
# GET or POST with invalid form data # GET or POST with invalid form data
return render_template("lexicon.join.jinja", form=form) return render_template(
"lexicon.join.jinja", lexicon_name=lexicon_name, form=form
)
# POST with valid data # POST with valid data
# If the game is passworded, check password # If the game is passworded, check password
@ -37,48 +43,48 @@ def join(name):
): ):
# Bad creds, try again # Bad creds, try again
flash("Incorrect password") flash("Incorrect password")
return redirect(url_for("lexicon.join", name=name)) return redirect(url_for("lexicon.join", lexicon_name=lexicon_name))
# If the password was correct, check if the user can join # If the password was correct, check if the user can join
user: User = current_user user: User = current_user
try: try:
memq.create(db, user.id, lexicon.id, is_editor=False) memq.create(db, user.id, lexicon.id, is_editor=False)
return redirect(url_for("session.session", name=name)) return redirect(url_for("session.session", lexicon_name=lexicon_name))
except ArgumentError: except ArgumentError:
flash("Could not join game") flash("Could not join game")
return redirect(url_for("home.home", name=name)) return redirect(url_for("home.home", lexicon_name=lexicon_name))
@bp.get("/contents/") @bp.get("/contents/")
@lexicon_param @lexicon_param
@player_required_if_not_public @player_required_if_not_public
def contents(name): def contents(lexicon_name):
# indexed = sort_by_index_spec(info, g.lexicon.cfg.article.index.list) # indexed = sort_by_index_spec(info, g.lexicon.cfg.article.index.list)
# for articles in indexed.values(): # for articles in indexed.values():
# for i in range(len(articles)): # for i in range(len(articles)):
# articles[i] = { # articles[i] = {
# 'title': articles[i], # 'title': articles[i],
# **info.get(articles[i])} # **info.get(articles[i])}
return render_template("lexicon.contents.jinja") return render_template("lexicon.contents.jinja", lexicon_name=lexicon_name)
@bp.get("/article/<title>") @bp.get("/article/<title>")
@lexicon_param @lexicon_param
@player_required_if_not_public @player_required_if_not_public
def article(name, title): def article(lexicon_name, title):
# article = {**a, 'html': Markup(a['html'])} # article = {**a, 'html': Markup(a['html'])}
return render_template("lexicon.article.jinja") return render_template("lexicon.article.jinja", lexicon_name=lexicon_name)
@bp.get("/rules/") @bp.get("/rules/")
@lexicon_param @lexicon_param
@player_required_if_not_public @player_required_if_not_public
def rules(name): def rules(lexicon_name):
return render_template("lexicon.rules.jinja") return render_template("lexicon.rules.jinja", lexicon_name=lexicon_name)
@bp.get("/statistics/") @bp.get("/statistics/")
@lexicon_param @lexicon_param
@player_required_if_not_public @player_required_if_not_public
def stats(name): def stats(lexicon_name):
return render_template("lexicon.statistics.jinja") return render_template("lexicon.statistics.jinja", lexicon_name=lexicon_name)

View File

@ -18,18 +18,18 @@ bp = Blueprint("characters", __name__, url_prefix="/characters", template_folder
@bp.get("/") @bp.get("/")
@lexicon_param @lexicon_param
@player_required @player_required
def list(name): def list(lexicon_name):
return render_template("characters.jinja", name=name) return render_template("characters.jinja", lexicon_name=lexicon_name)
@bp.route("/edit/<uuid:character_id>", methods=["GET", "POST"]) @bp.route("/edit/<uuid:character_id>", methods=["GET", "POST"])
@lexicon_param @lexicon_param
@player_required @player_required
def edit(name, character_id: uuid.UUID): def edit(lexicon_name, character_id: uuid.UUID):
character: Optional[Character] = charq.try_from_public_id(g.db, character_id) character: Optional[Character] = charq.try_from_public_id(g.db, character_id)
if not character: if not character:
flash("Character not found") flash("Character not found")
return redirect(url_for("lexicon.characters.list", name=name)) return redirect(url_for("lexicon.characters.list", lexicon_name=lexicon_name))
form = CharacterCreateForm() form = CharacterCreateForm()
@ -37,7 +37,12 @@ def edit(name, character_id: uuid.UUID):
# GET # GET
form.name.data = character.name form.name.data = character.name
form.signature.data = character.signature form.signature.data = character.signature
return render_template("characters.edit.jinja", character=character, form=form) return render_template(
"characters.edit.jinja",
lexicon_name=lexicon_name,
character=character,
form=form,
)
else: else:
# POST # POST
@ -45,25 +50,34 @@ def edit(name, character_id: uuid.UUID):
# Data is valid # Data is valid
character.name = form.name.data character.name = form.name.data
character.signature = form.signature.data character.signature = form.signature.data
g.db.session.commit() g.db.session.commit() # TODO refactor into backend
return redirect(url_for("lexicon.characters.list", name=name)) return redirect(
url_for("lexicon.characters.list", lexicon_name=lexicon_name)
)
else: else:
# POST submitted invalid data # POST submitted invalid data
return render_template( return render_template(
"characters.edit.jinja", character=character, form=form "characters.edit.jinja",
lexicon_name=lexicon_name,
character=character,
form=form,
) )
@bp.get("/new/") @bp.get("/new/")
@lexicon_param @lexicon_param
@player_required @player_required
def new(name): def new(lexicon_name):
dummy_name = f"{current_user.username}'s new character" dummy_name = f"{current_user.username}'s new character"
dummy_signature = "~" dummy_signature = "~"
char = charq.create( char = charq.create(
g.db, g.lexicon.id, current_user.id, dummy_name, dummy_signature g.db, g.lexicon.id, current_user.id, dummy_name, dummy_signature
) )
return redirect( return redirect(
url_for("lexicon.characters.edit", name=name, character_id=char.public_id) url_for(
"lexicon.characters.edit",
lexicon_name=lexicon_name,
character_id=char.public_id,
)
) )

View File

@ -13,7 +13,7 @@
<ul class="blockitem-list"> <ul class="blockitem-list">
{% if characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count < g.lexicon.character_limit %} {% if characters|map(attribute="user_id")|select("equalto", current_user.id)|list|count < g.lexicon.character_limit %}
<li> <li>
<h3><a href="{{ url_for('lexicon.characters.new', name=name) }}">Create a new character</a></h3> <h3><a href="{{ url_for('lexicon.characters.new', lexicon_name=lexicon_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> <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> </li>
{% endif %} {% endif %}
@ -25,7 +25,7 @@
{% endif %} {% endif %}
<p>Player: {{ character.user.username }}</p> <p>Player: {{ character.user.username }}</p>
{% if character.user == current_user %} {% 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> <p><a href="{{ url_for('lexicon.characters.edit', lexicon_name=lexicon_name, character_id=character.public_id) }}">Edit this character</a></p>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,145 @@
from flask import Blueprint, render_template, url_for, g, flash, redirect
from amanuensis.backend import *
from amanuensis.db import *
from amanuensis.server.helpers import (
editor_required,
lexicon_param,
player_required,
current_membership,
current_lexicon,
)
from .forms import PlayerSettingsForm, SetupSettingsForm
bp = Blueprint("settings", __name__, url_prefix="/settings", template_folder=".")
@bp.get("/")
@lexicon_param
@player_required
def page(lexicon_name):
return redirect(url_for("lexicon.settings.player", lexicon_name=lexicon_name))
@bp.route("/player/", methods=["GET", "POST"])
@lexicon_param
@player_required
def player(lexicon_name):
form = PlayerSettingsForm()
mem: Membership = current_membership
if not form.is_submitted():
# GET
form.notify_ready.data = mem.notify_ready
form.notify_reject.data = mem.notify_reject
form.notify_approve.data = mem.notify_approve
return render_template(
"settings.jinja",
lexicon_name=lexicon_name,
page_name=player.__name__,
form=form,
)
else:
# POST
if form.validate():
# Data is valid
mem.notify_ready = form.notify_ready.data
mem.notify_reject = form.notify_reject.data
mem.notify_approve = form.notify_approve.data
g.db.session.commit() # TODO refactor into backend
flash("Settings saved")
return redirect(
url_for("lexicon.settings.player", lexicon_name=lexicon_name)
)
else:
# Invalid POST data
return render_template(
"settings.jinja",
lexicon_name=lexicon_name,
page_name=player.__name__,
form=form,
)
@bp.route("/setup/", methods=["GET", "POST"])
@lexicon_param
@editor_required
def setup(lexicon_name):
form = SetupSettingsForm()
lexicon: Lexicon = current_lexicon
if not form.is_submitted():
# GET
form.title.data = lexicon.title
form.prompt.data = lexicon.prompt
form.public.data = lexicon.public
form.joinable.data = lexicon.joinable
form.has_password.data = lexicon.join_password is not None
form.turn_count.data = lexicon.turn_count
form.player_limit.data = lexicon.player_limit
form.character_limit.data = lexicon.character_limit
return render_template(
"settings.jinja",
lexicon_name=lexicon_name,
page_name=setup.__name__,
form=form,
)
else:
# POST
if form.validate():
# Data is valid
lexicon.title = form.title.data
lexicon.prompt = form.prompt.data
lexicon.public = form.public.data
lexicon.joinable = form.joinable.data
new_password = form.password.data if form.has_password.data else None
lexiq.password_set(g.db, lexicon.id, new_password)
lexicon.turn_count = form.turn_count.data
lexicon.player_limit = form.player_limit.data
lexicon.character_limit = form.character_limit.data
g.db.session.commit() # TODO refactor into backend
flash("Settings saved")
return redirect(
url_for("lexicon.settings.setup", lexicon_name=lexicon_name)
)
else:
# Invalid POST data
return render_template(
"settings.jinja",
lexicon_name=lexicon_name,
page_name=setup.__name__,
form=form,
)
@bp.get("/progress/")
@lexicon_param
@editor_required
def progress(lexicon_name):
return render_template(
"settings.jinja", lexicon_name=lexicon_name, page_name=progress.__name__
)
@bp.get("/publish/")
@lexicon_param
@editor_required
def publish(lexicon_name):
return render_template(
"settings.jinja", lexicon_name=lexicon_name, page_name=publish.__name__
)
@bp.get("/article/")
@lexicon_param
@editor_required
def article(lexicon_name):
return render_template(
"settings.jinja", lexicon_name=lexicon_name, page_name=article.__name__
)

View File

@ -0,0 +1,45 @@
from flask_wtf import FlaskForm
from wtforms import (
BooleanField,
IntegerField,
PasswordField,
StringField,
SubmitField,
TextAreaField,
)
from wtforms.validators import Optional, DataRequired
from wtforms.widgets.html5 import NumberInput
class PlayerSettingsForm(FlaskForm):
"""/lexicon/<name>/settings/player/"""
notify_ready = BooleanField("Notify me when an article is submitted for review")
notify_reject = BooleanField("Notify me when an editor rejects one of my articles")
notify_approve = BooleanField(
"Notify me when an editor approves one of my articles"
)
submit = SubmitField("Submit")
class SetupSettingsForm(FlaskForm):
"""/lexicon/<name>/settings/setup/"""
title = StringField("Title override")
prompt = TextAreaField("Prompt", validators=[DataRequired()])
public = BooleanField("Make game publicly visible")
joinable = BooleanField("Allow players to join game")
has_password = BooleanField("Require password to join the game")
password = PasswordField("Game password")
turn_count = IntegerField(
"Number of turns", widget=NumberInput(), validators=[DataRequired()]
)
player_limit = IntegerField(
"Maximum number of players", widget=NumberInput(), validators=[Optional()]
)
character_limit = IntegerField(
"Maximum number of characters per player",
widget=NumberInput(),
validators=[Optional()],
)
submit = SubmitField("Submit")

View File

@ -0,0 +1,99 @@
{% extends "lexicon.jinja" %}
{% block title %}Edit | {{ lexicon_title }}{% endblock %}
{% macro settings_page_link(page, text) -%}
<a{% if page_name != page %} href="{{ url_for('lexicon.settings.' + page, lexicon_name=lexicon_name) }}"{% endif %}>{{ text }}</a>
{%- endmacro %}
{% macro flag_setting(field) %}
{{ field() }}
{{ field.label }}<br>
{% endmacro %}
{% macro number_setting(field) %}
{{ field(autocomplete="off", class_="smallnumber") }}
{{ field.label }}<br>
{% for error in field.errors %}
<span style="color: #ff0000">{{ error }}</span><br>
{% endfor %}
{% endmacro %}
{% block main %}
{% if current_membership.is_editor %}
<ul class="unordered-tabs">
<li>{{ settings_page_link("player", "Player") }}</li>
<li>{{ settings_page_link("setup", "Game Setup") }}</li>
<li>{{ settings_page_link("progress", "Game Progress") }}</li>
<li>{{ settings_page_link("publish", "Turn Publishing") }}</li>
<li>{{ settings_page_link("article", "Article Requirements") }}</li>
</ul>
{% endif %}
{% if page_name == "player" %}
<h3>Player Settings</h3>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{% if current_membership.is_editor %}{{ flag_setting(form.notify_ready) }}{% endif %}
{{ flag_setting(form.notify_reject) }}
{{ flag_setting(form.notify_approve) }}
</p>
<p>{{ form.submit() }}</p>
</form>
{% for message in get_flashed_messages() %}
<span style="color:#ff0000">{{ message }}</span><br>
{% endfor %}
{% endif %}
{% if page_name == "setup" %}
<h3>Game Setup</h3>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.title.label }}:<br>
{{ form.title(autocomplete="off", placeholder="Lexicon " + lexicon_name, class_="fullwidth") }}<br>
</p>
<p>
{{ form.prompt.label }}: {{ form.prompt(class_="fullwidth") }}
{% for error in form.prompt.errors %}
<span style="color: #ff0000">{{ error }}</span><br>
{% endfor %}
</p>
<p>
{{ flag_setting(form.public) }}
{{ flag_setting(form.joinable) }}
{{ form.has_password() }}
{{ form.has_password.label }}:<br>
{{ form.password(autocomplete="off") }}
</p>
<p>
{{ number_setting(form.turn_count) }}
</p>
<p>
{{ number_setting(form.player_limit) }}
</p>
<p>
{{ number_setting(form.character_limit) }}
</p>
<p>{{ form.submit() }}</p>
</form>
{% for message in get_flashed_messages() %}
<span style="color:#ff0000">{{ message }}</span><br>
{% endfor %}
{% endif %}
{% if page_name == "progress" %}
<h3>Game Progress</h3>
{% endif %}
{% if page_name == "publish" %}
<h3>Turn Publishing</h3>
{% endif %}
{% if page_name == "article" %}
<h3>Article Requirements</h3>
{% endif %}
{% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -3,7 +3,7 @@
<div class="dashboard-lexicon-item dashboard-lexicon-{{ status }}"> <div class="dashboard-lexicon-item dashboard-lexicon-{{ status }}">
<p> <p>
<span class="dashboard-lexicon-item-title"> <span class="dashboard-lexicon-item-title">
<a href="{{ url_for('lexicon.contents', name=lexicon.name) }}">{{ lexicon.full_title }}</a> <a href="{{ url_for('lexicon.contents', lexicon_name=lexicon.name) }}">{{ lexicon.full_title }}</a>
</span> </span>
[{{ status.capitalize() }}] [{{ status.capitalize() }}]
</p> </p>
@ -29,7 +29,7 @@
Players: {{ lexicon.memberships|count }}{% if lexicon.player_limit is not none %} / {{ lexicon.player_limit }}{% endif -%} Players: {{ lexicon.memberships|count }}{% if lexicon.player_limit is not none %} / {{ lexicon.player_limit }}{% endif -%}
{%- {%-
if lexicon.public and lexicon.joinable if lexicon.public and lexicon.joinable
%} / <a href="{{ url_for('lexicon.join', name=lexicon.name) }}">Join game</a> %} / <a href="{{ url_for('lexicon.join', lexicon_name=lexicon.name) }}">Join game</a>
{%- endif -%} {%- endif -%}
{%- endif -%} {%- endif -%}
</p> </p>

View File

@ -32,12 +32,12 @@ def test_character_view(db: DbContext, app: Flask, make: ObjectFactory):
assert mem assert mem
# The character page exists # The character page exists
list_url = url_for("lexicon.characters.list", name=lexicon.name) list_url = url_for("lexicon.characters.list", lexicon_name=lexicon.name)
response = client.get(list_url) response = client.get(list_url)
assert response.status_code == 200 assert response.status_code == 200
assert charname.encode("utf8") not in response.data assert charname.encode("utf8") not in response.data
assert char_sig.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) new_url = url_for("lexicon.characters.new", lexicon_name=lexicon.name)
assert new_url.encode("utf8") in response.data assert new_url.encode("utf8") in response.data
# The character creation endpoint works # The character creation endpoint works