Compare commits

...

5 Commits

28 changed files with 292 additions and 332 deletions

View File

@ -0,0 +1,9 @@
import amanuensis.backend.article as artiq
import amanuensis.backend.character as charq
import amanuensis.backend.index as indq
import amanuensis.backend.lexicon as lexiq
import amanuensis.backend.membership as memq
import amanuensis.backend.post as postq
import amanuensis.backend.user as userq
__all__ = ["artiq", "charq", "indq", "lexiq", "memq", "postq", "userq"]

View File

@ -5,7 +5,8 @@ Lexicon query interface
import re
from typing import Sequence, Optional
from sqlalchemy import select, func
from sqlalchemy import select, func, update
from werkzeug.security import generate_password_hash, check_password_hash
from amanuensis.db import DbContext, Lexicon, Membership
from amanuensis.errors import ArgumentError
@ -55,11 +56,6 @@ def create(
return new_lexicon
def from_name(db: DbContext, name: str) -> Lexicon:
"""Get a lexicon by its name."""
return db(select(Lexicon).where(Lexicon.name == name)).scalar_one()
def get_all(db: DbContext) -> Sequence[Lexicon]:
"""Get all lexicons."""
return db(select(Lexicon)).scalars()
@ -75,3 +71,23 @@ def get_joined(db: DbContext, user_id: int) -> Sequence[Lexicon]:
def get_public(db: DbContext) -> Sequence[Lexicon]:
"""Get all publicly visible lexicons."""
return db(select(Lexicon).where(Lexicon.public == True)).scalars()
def password_check(db: DbContext, lexicon_id: int, password: str) -> bool:
"""Check if a password is correct."""
password_hash: str = db(
select(Lexicon.join_password).where(Lexicon.id == lexicon_id)
).scalar_one()
return check_password_hash(password_hash, password)
def password_set(db: DbContext, lexicon_id: int, new_password: Optional[str]) -> None:
"""Set or clear a lexicon's password."""
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.session.commit()
def try_from_name(db: DbContext, name: str) -> Optional[Lexicon]:
"""Get a lexicon by its name, or None if no such lexicon was found."""
return db(select(Lexicon).where(Lexicon.name == name)).scalar_one_or_none()

View File

@ -64,3 +64,12 @@ def create(
db.session.add(new_membership)
db.session.commit()
return new_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."""
return db(
select(Membership)
.where(Membership.user_id == user_id)
.where(Membership.lexicon_id == lexicon_id)
).scalar_one_or_none()

View File

@ -71,36 +71,11 @@ def create(
return new_user
def from_id(db: DbContext, user_id: int) -> Optional[User]:
"""
Get a user by the user's id.
Returns None if no user was found.
"""
user: User = db(select(User).where(User.id == user_id)).scalar_one_or_none()
return user
def from_username(db: DbContext, username: str) -> Optional[User]:
"""
Get a user by the user's username.
Returns None if no user was found.
"""
user: User = db(select(User).where(User.username == username)).scalar_one_or_none()
return user
def get_all(db: DbContext) -> Sequence[User]:
"""Get all users."""
return db(select(User)).scalars()
def password_set(db: DbContext, username: str, new_password: str) -> None:
"""Set a user's password."""
password_hash = generate_password_hash(new_password)
db(update(User).where(User.username == username).values(password=password_hash))
db.session.commit()
def password_check(db: DbContext, username: str, password: str) -> bool:
"""Check if a password is correct."""
user_password_hash: str = db(
@ -109,6 +84,23 @@ def password_check(db: DbContext, username: str, password: str) -> bool:
return check_password_hash(user_password_hash, password)
def password_set(db: DbContext, username: str, new_password: str) -> None:
"""Set a user's password."""
password_hash = generate_password_hash(new_password)
db(update(User).where(User.username == username).values(password=password_hash))
db.session.commit()
def try_from_id(db: DbContext, user_id: int) -> Optional[User]:
"""Get a user by the user's id, or None is no such user was found."""
return db(select(User).where(User.id == user_id)).scalar_one_or_none()
def try_from_username(db: DbContext, username: str) -> Optional[User]:
"""Get a user by the user's username, or None is no such user was found."""
return db(select(User).where(User.username == username)).scalar_one_or_none()
def update_logged_in(db: DbContext, username: str) -> None:
"""Bump the value of the last_login column for a user."""
db(

View File

@ -2,9 +2,7 @@ import logging
from sqlalchemy import update
import amanuensis.backend.lexicon as lexiq
import amanuensis.backend.membership as memq
import amanuensis.backend.user as userq
from amanuensis.backend import lexiq, memq, userq
from amanuensis.db import DbContext, Lexicon
from .helpers import add_argument
@ -24,9 +22,12 @@ def command_add(args) -> int:
Add a user to a lexicon.
"""
db: DbContext = args.get_db()
lexicon = lexiq.from_name(db, args.lexicon)
user = userq.from_username(db, args.user)
assert user is not None
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")
memq.create(db, user.id, lexicon.id, args.editor)
LOG.info(f"Added {args.user} to lexicon {args.lexicon}")
return 0

View File

@ -1,7 +1,7 @@
import logging
from typing import Optional
import amanuensis.backend.user as userq
from amanuensis.backend import userq
from amanuensis.db import DbContext, User
from .helpers import add_argument
@ -29,7 +29,7 @@ def command_create(args) -> int:
def command_promote(args) -> int:
"""Make a user a site admin."""
db: DbContext = args.get_db()
user: Optional[User] = userq.from_username(db, args.username)
user: Optional[User] = userq.try_from_username(db, args.username)
if user is None:
args.parser.error("User not found")
return -1
@ -46,7 +46,7 @@ def command_promote(args) -> int:
def command_demote(args):
"""Revoke a user's site admin status."""
db: DbContext = args.get_db()
user: Optional[User] = userq.from_username(db, args.username)
user: Optional[User] = userq.try_from_username(db, args.username)
if user is None:
args.parser.error("User not found")
return -1

View File

@ -11,58 +11,6 @@ from amanuensis.models import LexiconModel, UserModel
from amanuensis.resources import get_stream
def player_can_join_lexicon(
player: UserModel,
lexicon: LexiconModel,
password: str = None) -> bool:
"""
Checks whether the given player can join a lexicon
"""
# Trivial failures
if lexicon is None:
return False
if player is None:
return False
# Can't join if already in the game
if player.uid in lexicon.cfg.join.joined:
return False
# Can't join if the game is closed
if not lexicon.cfg.join.open:
return False
# Can't join if there's no room left
if len(lexicon.cfg.join.joined) >= lexicon.cfg.join.max_players:
return False
# Can't join if the password doesn't check out
if (lexicon.cfg.join.password is not None
and lexicon.cfg.join.password != password):
return False
return True
def add_player_to_lexicon(
player: UserModel,
lexicon: LexiconModel) -> None:
"""
Unconditionally adds a player to a lexicon
"""
# Verify arguments
if lexicon is None:
raise ArgumentError(f'Invalid lexicon: {lexicon}')
if player is None:
raise ArgumentError(f'Invalid player: {player}')
# Idempotently add player
added = False
with lexicon.ctx.edit_config() as cfg:
if player.uid not in cfg.join.joined:
cfg.join.joined.append(player.uid)
added = True
# Log to the lexicon's log
if added:
lexicon.log('Player "{0.cfg.username}" joined ({0.uid})'.format(player))
def player_can_create_character(
player: UserModel,
lexicon: LexiconModel,

View File

@ -2,14 +2,32 @@ from datetime import datetime, timezone
import json
import os
from flask import Flask, g
from flask import Flask, g, url_for, redirect
import amanuensis.backend.lexicon
import amanuensis.backend.user
from amanuensis.backend import lexiq, userq, memq
from amanuensis.config import AmanuensisConfig, CommandLineConfig
from amanuensis.db import DbContext
from amanuensis.parser import filesafe_title
import amanuensis.server.auth as auth
import amanuensis.server.home as home
import amanuensis.server.lexicon as lexicon
def date_format(dt: datetime, formatstr="%Y-%m-%d %H:%M:%S%z") -> str:
"""Convert datetime to human-readable string"""
if dt is None:
return "never"
# Cast db time to UTC, then convert to local timezone
adjusted = dt.replace(tzinfo=timezone.utc).astimezone()
return adjusted.strftime(formatstr)
def article_link(title):
"""Get the url for a lexicon by its title"""
return url_for(
'lexicon.article',
name=g.lexicon.name,
title=filesafe_title(title))
def get_app(
@ -49,20 +67,12 @@ def get_app(
app.teardown_appcontext(db_teardown)
# Configure jinja options
app.jinja_options.update(trim_blocks=True, lstrip_blocks=True)
def date_format(dt: datetime, formatstr="%Y-%m-%d %H:%M:%S%z") -> str:
if dt is None:
return "never"
# Cast db time to UTC, then convert to local timezone
adjusted = dt.replace(tzinfo=timezone.utc).astimezone()
return adjusted.strftime(formatstr)
app.template_filter("date")(date_format)
def include_backend():
return {"db": db, "lexiq": amanuensis.backend.lexicon, "userq": amanuensis.backend.user}
return {"db": db, "lexiq": lexiq, "userq": userq, "memq": memq}
app.jinja_options.update(trim_blocks=True, lstrip_blocks=True)
app.template_filter("date")(date_format)
app.template_filter("articlelink")(article_link)
app.context_processor(include_backend)
# Set up Flask-Login
@ -71,11 +81,13 @@ def get_app(
# Register blueprints
app.register_blueprint(auth.bp)
app.register_blueprint(home.bp)
app.register_blueprint(lexicon.bp)
def test():
return "Hello, world!"
# Add a root redirect
def root():
return redirect(url_for("home.home"))
app.route("/")(test)
app.route("/")(root)
return app

View File

@ -17,7 +17,7 @@ from flask_login import (
LoginManager,
)
import amanuensis.backend.user as userq
from amanuensis.backend import userq
from amanuensis.db import User
from .forms import LoginForm
@ -39,7 +39,7 @@ def get_login_manager() -> LoginManager:
user_id = int(user_id_str)
except:
return None
return userq.from_id(g.db, user_id)
return userq.try_from_id(g.db, user_id)
login_manager.user_loader(load_user)
@ -58,7 +58,7 @@ def login():
# POST with valid data
username: str = form.username.data
password: str = form.password.data
user: User = userq.from_username(g.db, username)
user: User = userq.try_from_username(g.db, username)
if not user or not userq.password_check(g.db, username, password):
# Bad creds
flash("Login not recognized")

View File

@ -1,107 +1,95 @@
# Standard library imports
from datetime import datetime
from functools import wraps
from typing import Optional
# Third party imports
from flask import g, flash, redirect, url_for, current_app
from flask import g, flash, redirect, url_for
from flask_login import current_user
# Module imports
from amanuensis.parser import filesafe_title
from amanuensis.models import ModelFactory, UserModel, LexiconModel
def register_custom_filters(app):
"""Adds custom filters to the Flask app"""
@app.template_filter("user_attr")
def get_user_attr(uid, attr):
factory: ModelFactory = current_app.config['model_factory']
user: UserModel = factory.user(uid)
val = getattr(user.cfg, attr)
return val
@app.template_filter("articlelink")
def article_link(title):
return url_for(
'lexicon.article',
name=g.lexicon.cfg.name,
title=filesafe_title(title))
@app.context_processor
def lexicon_status():
return dict(
PREGAME=LexiconModel.PREGAME,
ONGOING=LexiconModel.ONGOING,
COMPLETE=LexiconModel.COMPLETE)
from amanuensis.backend import lexiq, memq
from amanuensis.db import DbContext, Lexicon, User, Membership
def lexicon_param(route):
"""Wrapper for loading a route's lexicon"""
@wraps(route)
def with_lexicon(**kwargs):
name = kwargs.get('name')
model_factory: ModelFactory = current_app.config['model_factory']
g.lexicon = model_factory.lexicon(name)
if g.lexicon is None:
flash(f'Couldn\'t find a lexicon with the name "{name}"')
return redirect(url_for("home.home"))
return route(**kwargs)
return with_lexicon
"""
Wrapper for loading a route's lexicon to `g`.
This decorator should be applied above any other decorators that reference `g.lexicon`.
"""
@wraps(route)
def with_lexicon(*args, **kwargs):
db: DbContext = g.db
name: str = kwargs.get('name')
lexicon: Optional[Lexicon] = lexiq.try_from_name(db, name)
if lexicon is None:
flash(f"Couldn't find a lexicon with the name \"{name}\"")
return redirect(url_for("home.home"))
g.lexicon = lexicon
return route(*args, **kwargs)
return with_lexicon
def admin_required(route):
"""
Requires the user to be an admin to load this page
"""
@wraps(route)
def admin_route(*args, **kwargs):
if not current_user.cfg.is_admin:
flash("You must be an admin to view this page")
return redirect(url_for('home.home'))
return route(*args, **kwargs)
return admin_route
"""
Restricts a route to users who are site admins.
"""
@wraps(route)
def admin_route(*args, **kwargs):
user: User = current_user
if not user.is_site_admin:
flash("You must be an admin to view this page")
return redirect(url_for('home.home'))
return route(*args, **kwargs)
return admin_route
def player_required(route):
"""
Requires the user to be a player in the lexicon to load this page
"""
@wraps(route)
def player_route(*args, **kwargs):
if current_user.uid not in g.lexicon.cfg.join.joined:
flash("You must be a player to view this page")
return (redirect(url_for('lexicon.contents', name=g.lexicon.cfg.name))
if g.lexicon.cfg.join.public
else redirect(url_for('home.home')))
return route(*args, **kwargs)
return player_route
"""
Restricts a route to users who are players in the current lexicon.
"""
@wraps(route)
def player_route(*args, **kwargs):
db: DbContext = g.db
user: User = current_user
lexicon: Lexicon = g.lexicon
mem: Optional[Membership] = memq.try_from_ids(db, user.id, lexicon.id)
if not mem:
flash("You must be a player to view this page")
if lexicon.public:
return redirect(url_for('lexicon.contents', name=lexicon.name))
else:
return redirect(url_for('home.home'))
return route(*args, **kwargs)
return player_route
def player_required_if_not_public(route):
"""
Requires the user to be a player in the lexicon to load this page if the
lexicon has join.public = false
"""
@wraps(route)
def player_route(*args, **kwargs):
if ((not g.lexicon.cfg.join.public)
and current_user.uid not in g.lexicon.cfg.join.joined):
flash("You must be a player to view this page")
return redirect(url_for('home.home'))
return route(*args, **kwargs)
return player_route
"""
Restricts a route to users who are players in the current lexicon if the lexicon is nonpublic.
"""
@wraps(route)
def player_route(*args, **kwargs):
db: DbContext = g.db
user: User = current_user
lexicon: Lexicon = g.lexicon
if not lexicon.public:
mem: Optional[Membership] = memq.try_from_ids(db, user.id, lexicon.id)
if not mem:
flash("You must be a player to view this page")
return redirect(url_for('home.home'))
return route(*args, **kwargs)
return player_route
def editor_required(route):
"""
Requires the user to be the editor of the current lexicon to load this
page
"""
@wraps(route)
def editor_route(*args, **kwargs):
if current_user.uid != g.lexicon.cfg.editor:
flash("You must be the editor to view this page")
return redirect(url_for('lexicon.contents', name=g.lexicon.cfg.name))
return route(*args, **kwargs)
return editor_route
"""
Restricts a route to users who are editors of the current lexicon.
"""
@wraps(route)
def editor_route(*args, **kwargs):
db: DbContext = g.db
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:
flash("You must be the editor to view this page")
return redirect(url_for('lexicon.contents', name=lexicon.name))
return route(*args, **kwargs)
return editor_route

View File

@ -1,7 +1,6 @@
from flask import Blueprint, render_template, g
import amanuensis.backend.user as userq
import amanuensis.backend.lexicon as lexiq
from amanuensis.backend import userq, lexiq
# from .forms import LexiconCreateForm

View File

@ -1,44 +1,44 @@
{% extends "page_2col.jinja" %}
{% set lexicon_title = g.lexicon.title %}
{% set lexicon_title = g.lexicon.full_title %}
{% block header %}
<h2>{{ lexicon_title }}</h2>
<p><i>{{ g.lexicon.cfg.prompt }}</i></p>
<p><i>{{ g.lexicon.prompt }}</i></p>
{% endblock %}
{% block sb_logo %}{% endblock %}
{% block sb_home %}<a href="{{ url_for('home.home') }}">Home</a>
{% endblock %}
{% block sb_contents %}<a
{% if current_page == "contents" %}class="current-page"
{% else %}href="{{ url_for('lexicon.contents', name=g.lexicon.cfg.name) }}"
{% endif %}>Contents</a>{% endblock %}
{% if current_page == "contents" %}class="current-page"
{% else %}href="{{ url_for('lexicon.contents', name=g.lexicon.name) }}"
{% endif %}>Contents</a>{% endblock %}
{% block sb_rules %}<a
{% if current_page == "rules" %}class="current-page"
{% else %}href="{{ url_for('lexicon.rules', name=g.lexicon.cfg.name) }}"
{% endif %}>Rules</a>{% endblock %}
{% if current_page == "rules" %}class="current-page"
{% else %}href="{{ url_for('lexicon.rules', name=g.lexicon.name) }}"
{% endif %}>Rules</a>{% endblock %}
{% block sb_session %}<a
{% if current_page == "session" %}class="current-page"
{% else %}href="{{ url_for('session.session', name=g.lexicon.cfg.name) }}"
{% endif %}>Session</a>{% endblock %}
{% if current_page == "session" %}class="current-page"
{% else %}href="#{#{ url_for('session.session', name=g.lexicon.name) }#}"
{% endif %}>Session</a>{% endblock %}
{% block sb_stats %}<a
{% if current_page == "statistics" %}class="current-page"
{% else %}href="{{ url_for('lexicon.stats', name=g.lexicon.cfg.name) }}"
{% endif %}>Statistics</a>{% endblock %}
{% if current_page == "statistics" %}class="current-page"
{% else %}href="{{ url_for('lexicon.stats', name=g.lexicon.name) }}"
{% endif %}>Statistics</a>{% endblock %}
{% if current_user.uid in g.lexicon.cfg.join.joined %}
{# self.sb_logo(), #}
{% if current_user.is_authenticated and memq.try_from_ids(g.db, current_user.id, g.lexicon.id) %}
{# self.sb_logo(), #}
{% set template_sidebar_rows = [
self.sb_home(),
self.sb_contents(),
self.sb_rules(),
self.sb_session(),
self.sb_stats()] %}
self.sb_home(),
self.sb_contents(),
self.sb_rules(),
self.sb_session(),
self.sb_stats()] %}
{% else %}
{# self.sb_logo(), #}
{# self.sb_logo(), #}
{% set template_sidebar_rows = [
self.sb_home(),
self.sb_contents(),
self.sb_rules(),
self.sb_stats()] %}
self.sb_home(),
self.sb_contents(),
self.sb_rules(),
self.sb_stats()] %}
{% endif %}

View File

@ -1,96 +1,82 @@
from flask import (
Blueprint,
flash,
redirect,
url_for,
g,
render_template,
Markup)
from flask import Blueprint, flash, redirect, url_for, g, render_template, Markup
from flask_login import login_required, current_user
from amanuensis.lexicon import (
player_can_join_lexicon,
add_player_to_lexicon,
sort_by_index_spec)
from amanuensis.models import LexiconModel
from amanuensis.server.helpers import (
lexicon_param,
player_required_if_not_public)
from amanuensis.backend import lexiq, memq
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 .forms import LexiconJoinForm
bp_lexicon = Blueprint('lexicon', __name__,
url_prefix='/lexicon/<name>',
template_folder='.')
bp = Blueprint("lexicon", __name__, url_prefix="/lexicon/<name>", template_folder=".")
@bp_lexicon.route("/join/", methods=['GET', 'POST'])
@bp.route("/join/", methods=["GET", "POST"])
@lexicon_param
@login_required
def join(name):
if g.lexicon.status != LexiconModel.PREGAME:
flash("Can't join a game already in progress")
return redirect(url_for('home.home'))
lexicon: Lexicon = g.lexicon
if not lexicon.joinable:
flash("This game isn't open for joining")
return redirect(url_for("home.home"))
if not g.lexicon.cfg.join.open:
flash("This game isn't open for joining")
return redirect(url_for('home.home'))
form = LexiconJoinForm()
form = LexiconJoinForm()
if not form.validate_on_submit():
# GET or POST with invalid form data
return render_template("lexicon.join.jinja", form=form)
if not form.validate_on_submit():
# GET or POST with invalid form data
return render_template('lexicon.join.jinja', form=form)
# POST with valid data
# If the game is passworded, check password
db: DbContext = g.db
if lexicon.join_password and not lexiq.password_check(
db, lexicon.id, form.password.data
):
# Bad creds, try again
flash("Incorrect password")
return redirect(url_for("lexicon.join", name=name))
# POST with valid data
# If the game is passworded, check password
if (g.lexicon.cfg.join.password
and form.password.data != g.lexicon.cfg.join.password):
# Bad creds, try again
flash('Incorrect password')
return redirect(url_for('lexicon.join', name=name))
# If the password was correct, check if the user can join
if player_can_join_lexicon(current_user, g.lexicon, form.password.data):
add_player_to_lexicon(current_user, g.lexicon)
return redirect(url_for('session.session', name=name))
else:
flash('Could not join game')
return redirect(url_for('home.home', name=name))
# If the password was correct, check if the user can join
user: User = current_user
try:
memq.create(db, user.id, lexicon.id, is_editor=False)
return redirect(url_for("session.session", name=name))
except ArgumentError:
flash("Could not join game")
return redirect(url_for("home.home", name=name))
@bp_lexicon.route('/contents/', methods=['GET'])
@bp.get("/contents/")
@lexicon_param
@player_required_if_not_public
def contents(name):
with g.lexicon.ctx.read('info') as info:
indexed = sort_by_index_spec(info, g.lexicon.cfg.article.index.list)
for articles in indexed.values():
for i in range(len(articles)):
articles[i] = {
'title': articles[i],
**info.get(articles[i])}
return render_template('lexicon.contents.jinja', indexed=indexed)
# indexed = sort_by_index_spec(info, g.lexicon.cfg.article.index.list)
# for articles in indexed.values():
# for i in range(len(articles)):
# articles[i] = {
# 'title': articles[i],
# **info.get(articles[i])}
return render_template("lexicon.contents.jinja")
@bp_lexicon.route('/article/<title>')
@bp.get("/article/<title>")
@lexicon_param
@player_required_if_not_public
def article(name, title):
with g.lexicon.ctx.article.read(title) as a:
article = {**a, 'html': Markup(a['html'])}
return render_template('lexicon.article.jinja', article=article)
# article = {**a, 'html': Markup(a['html'])}
return render_template("lexicon.article.jinja")
@bp_lexicon.route('/rules/', methods=['GET'])
@bp.get("/rules/")
@lexicon_param
@player_required_if_not_public
def rules(name):
return render_template('lexicon.rules.jinja')
return render_template("lexicon.rules.jinja")
@bp_lexicon.route('/statistics/', methods=['GET'])
@bp.get("/statistics/")
@lexicon_param
@player_required_if_not_public
def stats(name):
return render_template('lexicon.statistics.jinja')
return render_template("lexicon.statistics.jinja")

View File

@ -3,6 +3,7 @@ from wtforms import StringField, SubmitField
class LexiconJoinForm(FlaskForm):
"""/lexicon/<name>/join/"""
password = StringField('Password')
submit = SubmitField('Submit')
"""/lexicon/<name>/join/"""
password = StringField("Password")
submit = SubmitField("Submit")

View File

@ -14,14 +14,14 @@
{% block citations %}
<p>
{% for citation in article.cites %}
<a href="{{ citation|articlelink }}">{{ citation }}</a>{% if not loop.last %} / {% endif %}
{% endfor %}
{% for citation in article.cites %}
<a href="{{ citation|articlelink }}">{{ citation }}</a>{% if not loop.last %} / {% endif %}
{% endfor %}
</p>
<p>
{% for citation in article.citedby %}
<a href="{{ citation|articlelink }}">{{ citation }}</a>{% if not loop.last %} / {% endif %}
{% endfor %}
{% for citation in article.citedby %}
<a href="{{ citation|articlelink }}">{{ citation }}</a>{% if not loop.last %} / {% endif %}
{% endfor %}
</p>
{% endblock %}

View File

@ -14,7 +14,7 @@
<ul>
{% for article in indexed[index] %}
<li><a href="{{ article.title|articlelink }}" class="{{ 'phantom' if not article.character else '' }}">
{{ article.title }}
{{ article.title }}
</a></li>
{% endfor %}
</ul>

View File

@ -1,14 +1,16 @@
{% extends "lexicon.jinja" %}
{% block title %}Join | {{ lexicon_title }}{% endblock %}
{% block title %}Join | {{ g.lexicon.full_title }}{% endblock %}
{% block main %}
<form id="lexicon-join" action="" method="post" novalidate>
{{ form.hidden_tag() }}
{% if g.lexicon.cfg.join.password %}
<p>{{ form.password.label }}<br>{{ form.password(size=32) }}</p>
{% endif %}
<p>{{ form.submit() }}</p>
{{ form.hidden_tag() }}
{% if g.lexicon.join_password %}
<p>{{ form.password.label }}<br>{{ form.password(size=32) }}</p>
{% else %}
<p>Join {{ g.lexicon.full_title }}?</p>
{% endif %}
<p>{{ form.submit() }}</p>
</form>
{% for message in get_flashed_messages() %}

View File

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

View File

@ -1,4 +1,4 @@
[mypy]
ignore_missing_imports = true
exclude = "|amanuensis/lexicon/.*|amanuensis/server/.*|amanuensis/server/lexicon/.*|amanuensis/server/session/.*|"
exclude = "|amanuensis/lexicon/.*|amanuensis/server/.*|amanuensis/server/session/.*|"
; mypy stable doesn't support pyproject.toml yet

View File

@ -22,11 +22,11 @@ amanuensis-cli = "amanuensis.cli:main"
amanuensis-server = "amanuensis.server:run"
[tool.black]
extend-exclude = "^/amanuensis/lexicon/.*|^/amanuensis/server/[^/]*py|^/amanuensis/server/lexicon/.*|^/amanuensis/server/session/.*|"
extend-exclude = "^/amanuensis/lexicon/.*|^/amanuensis/server/[^/]*py|^/amanuensis/server/session/.*|"
[tool.mypy]
ignore_missing_imports = true
exclude = "|amanuensis/lexicon/.*|amanuensis/server/.*|amanuensis/server/lexicon/.*|amanuensis/server/session/.*|"
exclude = "|amanuensis/lexicon/.*|amanuensis/server/.*|amanuensis/server/session/.*|"
[tool.pytest.ini_options]
addopts = "--show-capture=log"

View File

@ -1,9 +1,9 @@
import pytest
import time
from amanuensis.db import DbContext
from amanuensis.db.models import Character, Lexicon, User
import amanuensis.backend.article as artiq
from amanuensis.backend import artiq
from amanuensis.db import DbContext, Character, Lexicon, User
from amanuensis.errors import ArgumentError
from tests.conftest import ObjectFactory

View File

@ -1,7 +1,7 @@
import pytest
from amanuensis.backend import charq
from amanuensis.db import *
import amanuensis.backend.character as charq
from amanuensis.errors import ArgumentError

View File

@ -1,8 +1,8 @@
from amanuensis.db.models import IndexType
import pytest
import amanuensis.backend.index as indq
from amanuensis.db import DbContext, Lexicon, User
from amanuensis.backend import indq
from amanuensis.db import DbContext, Lexicon
from amanuensis.errors import ArgumentError

View File

@ -3,7 +3,7 @@ import time
import pytest
import amanuensis.backend.lexicon as lexiq
from amanuensis.backend import lexiq
from amanuensis.db import DbContext, Lexicon, User
from amanuensis.errors import ArgumentError
from tests.conftest import ObjectFactory
@ -58,8 +58,8 @@ def test_lexicon_from(db: DbContext, make: ObjectFactory):
"""Test lexiq.from_*."""
lexicon1: Lexicon = make.lexicon()
lexicon2: Lexicon = make.lexicon()
assert lexiq.from_name(db, lexicon1.name) == lexicon1
assert lexiq.from_name(db, lexicon2.name) == lexicon2
assert lexiq.try_from_name(db, lexicon1.name) == lexicon1
assert lexiq.try_from_name(db, lexicon2.name) == lexicon2
def test_get_lexicon(db: DbContext, make: ObjectFactory):

View File

@ -2,9 +2,9 @@ import pytest
from sqlalchemy import select
from amanuensis.backend import memq
from amanuensis.db import *
from amanuensis.errors import ArgumentError
import amanuensis.backend.membership as memq
def test_create_membership(db: DbContext, make):

View File

@ -1,7 +1,7 @@
import pytest
from amanuensis.backend import postq
from amanuensis.db import DbContext
import amanuensis.backend.post as postq
from amanuensis.errors import ArgumentError

View File

@ -2,7 +2,7 @@ import os
import pytest
import amanuensis.backend.user as userq
from amanuensis.backend import userq
from amanuensis.db import DbContext, User
from amanuensis.errors import ArgumentError
@ -57,10 +57,10 @@ def test_user_from(db: DbContext, make):
"""Test userq.from_*."""
user1: User = make.user()
user2: User = make.user()
assert userq.from_id(db, user1.id) == user1
assert userq.from_username(db, user1.username) == user1
assert userq.from_id(db, user2.id) == user2
assert userq.from_username(db, user2.username) == user2
assert userq.try_from_id(db, user1.id) == user1
assert userq.try_from_username(db, user1.username) == user1
assert userq.try_from_id(db, user2.id) == user2
assert userq.try_from_username(db, user2.username) == user2
def test_user_password(db: DbContext, make):

View File

@ -10,10 +10,7 @@ from bs4 import BeautifulSoup
from flask.testing import FlaskClient
from sqlalchemy.orm.session import close_all_sessions
import amanuensis.backend.character as charq
import amanuensis.backend.lexicon as lexiq
import amanuensis.backend.membership as memq
import amanuensis.backend.user as userq
from amanuensis.backend import charq, lexiq, memq, userq
from amanuensis.config import AmanuensisConfig
from amanuensis.db import DbContext, User, Lexicon, Membership, Character
from amanuensis.server import get_app
@ -45,7 +42,7 @@ class UserClient:
def login(self, client: FlaskClient):
"""Log the user in."""
user: Optional[User] = userq.from_id(self.db, self.user_id)
user: Optional[User] = userq.try_from_id(self.db, self.user_id)
assert user is not None
# Set the user's password so we know what it is later