Compare commits

..

10 Commits

Author SHA1 Message Date
Tim Van Baak 8754823556 Linter pass 2021-10-01 17:19:05 -07:00
Tim Van Baak d172848231 Respect lexicon post enable setting 2021-10-01 17:02:54 -07:00
Tim Van Baak f88ee3a526 Fix some pages not being selected properly in the sidebar 2021-10-01 16:52:59 -07:00
Tim Van Baak ab2f0edcb1 Fix errors caused by anonymous user model 2021-10-01 16:45:02 -07:00
Tim Van Baak 19e7af7e58 Show unread count in post sidebar link 2021-10-01 16:40:27 -07:00
Tim Van Baak 3cc487f94b Distinguish new posts from already-seen posts 2021-09-24 18:11:54 -07:00
Tim Van Baak b749357657 Add post feed page 2021-09-22 08:01:11 -07:00
Tim Van Baak 1ca46d47f5 Add post create cli 2021-09-20 21:25:10 -07:00
Tim Van Baak a6399e7e22 Refactor template content blocks for greater flexibility
There is not much value to be gotten out of creating Jinja blocks and
appending them to a list when nothing particularly interesting is done
with the list. With changes to move more towards semantic HTML, as well
as more ease of access to data in the template engine in the new code,
it is preferable to leave block division to the page template by making
it a property of the <section> tag. This also allows creating blocks in
Jinja iterators, which is not possible to do cleanly in the idiom being
replaced here.
2021-09-20 20:11:40 -07:00
Tim Van Baak ffaf881707 Replace divs with semantic elements 2021-09-20 18:43:55 -07:00
32 changed files with 370 additions and 154 deletions

View File

@ -2,19 +2,19 @@
Post query interface Post query interface
""" """
import re from typing import Optional, Sequence, Tuple
from sqlalchemy import select from sqlalchemy import select, update, func, or_, DateTime
from amanuensis.db import DbContext, Post from amanuensis.db import DbContext, Post
from amanuensis.db.models import Lexicon from amanuensis.db.models import Lexicon, Membership
from amanuensis.errors import ArgumentError, BackendArgumentTypeError from amanuensis.errors import ArgumentError, BackendArgumentTypeError
def create( def create(
db: DbContext, db: DbContext,
lexicon_id: int, lexicon_id: int,
user_id: int, user_id: Optional[int],
body: str, body: str,
) -> Post: ) -> Post:
""" """
@ -47,3 +47,57 @@ def create(
db.session.add(new_post) db.session.add(new_post)
db.session.commit() db.session.commit()
return new_post return new_post
def get_posts_for_membership(
db: DbContext, membership_id: int
) -> Tuple[Sequence[Post], Sequence[Post]]:
"""
Returns posts for the membership's lexicon, split into posts that
are new since the last view and posts that were previously seen.
"""
# Save the current timestamp, so we don't miss posts created between now
# and when we finish looking stuff up
now: DateTime = db(select(func.now())).scalar_one()
# Save the previous last-seen timestamp for splitting new from old posts,
# then update the membership with the current time
last_seen: DateTime = db(
select(Membership.last_post_seen).where(Membership.id == membership_id)
).scalar_one()
db(
update(Membership)
.where(Membership.id == membership_id)
.values(last_post_seen=now)
)
db.session.commit()
# Fetch posts in two groups, new ones after the last-seen time and old ones
# If last-seen is null, then just return everything as new
new_posts = db(
select(Post)
.where(last_seen is None or Post.created > last_seen)
.order_by(Post.created.desc())
).scalars()
old_posts = db(
select(Post)
.where(last_seen is not None and Post.created <= last_seen)
.order_by(Post.created.desc())
).scalars()
return new_posts, old_posts
def get_unread_count(db: DbContext, membership_id: int) -> int:
"""Get the number of posts that the member has not seen"""
return db(
select(func.count(Post.id))
.join(Membership, Membership.lexicon_id == Post.lexicon_id)
.where(
or_(
Membership.last_post_seen.is_(None),
Post.created > Membership.last_post_seen,
)
)
.where(Membership.id == membership_id)
).scalar()

View File

@ -8,6 +8,7 @@ import amanuensis.cli.admin
import amanuensis.cli.character import amanuensis.cli.character
import amanuensis.cli.index import amanuensis.cli.index
import amanuensis.cli.lexicon import amanuensis.cli.lexicon
import amanuensis.cli.post
import amanuensis.cli.user import amanuensis.cli.user
from amanuensis.db import DbContext from amanuensis.db import DbContext
@ -113,6 +114,7 @@ def main():
add_subcommand(subparsers, amanuensis.cli.character) add_subcommand(subparsers, amanuensis.cli.character)
add_subcommand(subparsers, amanuensis.cli.index) add_subcommand(subparsers, amanuensis.cli.index)
add_subcommand(subparsers, amanuensis.cli.lexicon) add_subcommand(subparsers, amanuensis.cli.lexicon)
add_subcommand(subparsers, amanuensis.cli.post)
add_subcommand(subparsers, amanuensis.cli.user) add_subcommand(subparsers, amanuensis.cli.user)
# Parse args and perform top-level arg processing # Parse args and perform top-level arg processing

31
amanuensis/cli/post.py Normal file
View File

@ -0,0 +1,31 @@
import logging
from amanuensis.backend import *
from amanuensis.db import *
from .helpers import add_argument
COMMAND_NAME = "post"
COMMAND_HELP = "Interact with posts."
LOG = logging.getLogger(__name__)
@add_argument("--lexicon", required=True, help="The lexicon's name")
@add_argument("--by", help="The character's public id")
@add_argument("--text", help="The text of the post")
def command_create(args) -> int:
"""
Create a post in a lexicon.
"""
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.by)
user_id = user.id if user else None
post: Post = postq.create(db, lexicon.id, user_id, args.text)
preview = post.body[:20] + "..." if len(post.body) > 20 else post.body
LOG.info(f"Posted '{preview}' in {lexicon.full_title}")
return 0

View File

@ -181,7 +181,7 @@ class Lexicon(ModelBase):
################################ ################################
# Whether players can join the game # Whether players can join the game
joinable = Column(Boolean, nullable=False, default=True) joinable = Column(Boolean, nullable=False, default=False)
# Whether the game is listed on public pages # Whether the game is listed on public pages
public = Column(Boolean, nullable=False, default=False) public = Column(Boolean, nullable=False, default=False)
@ -251,7 +251,9 @@ class Lexicon(ModelBase):
indices = relationship("ArticleIndex", back_populates="lexicon") indices = relationship("ArticleIndex", back_populates="lexicon")
index_rules = relationship("ArticleIndexRule", back_populates="lexicon") index_rules = relationship("ArticleIndexRule", back_populates="lexicon")
content_rules = relationship("ArticleContentRule", back_populates="lexicon") content_rules = relationship("ArticleContentRule", back_populates="lexicon")
posts = relationship("Post", back_populates="lexicon") posts = relationship(
"Post", back_populates="lexicon", order_by="Post.created.desc()"
)
####################### #######################
# Derived information # # Derived information #

View File

@ -17,7 +17,7 @@ div#editor-left {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
} }
div#editor-left div.contentblock { div#editor-left section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 10px 5px 10px 10px; margin: 10px 5px 10px 10px;
@ -48,7 +48,7 @@ textarea#editor-content {
div#editor-right { div#editor-right {
overflow-y: scroll; overflow-y: scroll;
} }
div#editor-right div.contentblock { div#editor-right section {
margin: 10px 5px 10px 10px; margin: 10px 5px 10px 10px;
} }
span.message-warning { span.message-warning {

View File

@ -8,14 +8,14 @@ body {
line-height: 1.4; line-height: 1.4;
font-size: 16px; font-size: 16px;
} }
div#wrapper { main {
max-width: 800px; max-width: 800px;
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
margin: 0 auto; margin: 0 auto;
} }
div#header { header {
padding: 5px; padding: 5px;
margin: 5px; margin: 5px;
background-color: #ffffff; background-color: #ffffff;
@ -24,7 +24,7 @@ div#header {
overflow: auto; overflow: auto;
position: relative; position: relative;
} }
div#header p, div#header h2 { header p, header h2 {
margin: 5px; margin: 5px;
} }
div#login-status { div#login-status {
@ -33,7 +33,7 @@ div#login-status {
top: 10px; top: 10px;
right: 10px; right: 10px;
} }
div#sidebar { nav {
width: 200px; width: 200px;
float:left; float:left;
margin:5px; margin:5px;
@ -46,7 +46,7 @@ div#sidebar {
img#logo { img#logo {
max-width: 200px; max-width: 200px;
} }
div#sidebar table { nav table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
} }
@ -63,32 +63,32 @@ table a {
border-radius: 5px; border-radius: 5px;
text-decoration: none; text-decoration: none;
} }
div#sidebar table a { nav table a {
justify-content: center; justify-content: center;
} }
table a:hover { table a:hover {
background-color: var(--button-hover); background-color: var(--button-hover);
} }
div#sidebar table a.current-page { nav table a.current-page {
background-color: var(--button-current); background-color: var(--button-current);
} }
div#sidebar table a.current-page:hover { nav table a.current-page:hover {
background-color: var(--button-current); background-color: var(--button-current);
} }
div#sidebar table td { nav table td {
padding: 0px; margin: 3px 0; padding: 0px; margin: 3px 0;
border-bottom: 8px solid transparent; border-bottom: 8px solid transparent;
} }
div#content { article {
margin: 5px; margin: 5px;
} }
div.content-2col { article.content-2col {
position: absolute; position: absolute;
right: 0px; right: 0px;
left: 226px; left: 226px;
max-width: 564px; max-width: 564px;
} }
div.contentblock { section {
background-color: #ffffff; background-color: #ffffff;
box-shadow: 2px 2px 10px #888888; box-shadow: 2px 2px 10px #888888;
margin-bottom: 5px; margin-bottom: 5px;
@ -215,27 +215,35 @@ details.setting-help {
#index-definition-table td input[type=number] { #index-definition-table td input[type=number] {
width: 4em; width: 4em;
} }
section.new-post {
padding: 9px;
border: 1px dashed red;
}
p.post-byline {
color: #606060;
text-align: right;
}
@media only screen and (max-width: 816px) { @media only screen and (max-width: 816px) {
div#wrapper { main {
padding: 5px; padding: 5px;
} }
div#header { header {
max-width: 554px; max-width: 554px;
margin: 0 auto; margin: 0 auto;
} }
div#sidebar { nav {
max-width: 548px; max-width: 548px;
width: inherit; width: inherit;
float: inherit; float: inherit;
margin: 10px auto; margin: 10px auto;
} }
div#content{ article{
margin: 10px auto; margin: 10px auto;
} }
div.content-1col { article.content-1col {
max-width: 564px; max-width: 564px;
} }
div.content-2col { article.content-2col {
max-width: 564px; max-width: 564px;
position: static; position: static;
right: inherit; right: inherit;

View File

@ -76,6 +76,7 @@ def get_app(
"memq": memq, "memq": memq,
"charq": charq, "charq": charq,
"indq": indq, "indq": indq,
"postq": postq,
"current_lexicon": current_lexicon, "current_lexicon": current_lexicon,
"current_membership": current_membership "current_membership": current_membership
} }

View File

@ -3,21 +3,22 @@
{% block header %}<h2>Amanuensis - Login</h2>{% endblock %} {% block header %}<h2>Amanuensis - Login</h2>{% endblock %}
{% block login_status_attr %}style="display:none"{% endblock %} {% block login_status_attr %}style="display:none"{% endblock %}
{% block main %} {% block main %}
<section>
<form action="" method="post" novalidate> <form action="" method="post" novalidate>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<p>{{ form.username.label }}<br>{{ form.username(size=32) }} <p>{{ form.username.label }}<br>{{ form.username(size=32) }}
{% for error in form.username.errors %} {% for error in form.username.errors %}
<br><span style="color: #ff0000">{{ error }}</span> <br><span style="color: #ff0000">{{ error }}</span>
{% endfor %}</p> {% endfor %}</p>
<p>{{ form.password.label }}<br>{{ form.password(size=32) }} <p>{{ form.password.label }}<br>{{ form.password(size=32) }}
{% for error in form.password.errors %} {% for error in form.password.errors %}
<br><span style="color: #ff0000">{{ error }}</span> <br><span style="color: #ff0000">{{ error }}</span>
{% endfor %}</p> {% endfor %}</p>
<p>{{ form.remember }} {{ form.remember.label }}</p> <p>{{ form.remember }} {{ form.remember.label }}</p>
<p>{{ form.submit() }}</p> <p>{{ form.submit() }}</p>
</form> </form>
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}
<span style="color: #ff0000">{{ message }}</span><br> <span style="color: #ff0000">{{ message }}</span><br>
{% endfor %} {% endfor %}
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -99,7 +99,7 @@ def admin_required(route):
@wraps(route) @wraps(route)
def admin_route(*args, **kwargs): def admin_route(*args, **kwargs):
user: User = current_user user: User = current_user
if not user.is_site_admin: if not user.is_authenticated or not user.is_site_admin:
flash("You must be an admin to view this page") flash("You must be an admin to view this page")
return redirect(url_for('home.home')) return redirect(url_for('home.home'))
return route(*args, **kwargs) return route(*args, **kwargs)
@ -114,7 +114,13 @@ def player_required(route):
def player_route(*args, **kwargs): def player_route(*args, **kwargs):
db: DbContext = g.db db: DbContext = g.db
user: User = current_user user: User = current_user
lexicon: Lexicon = g.lexicon lexicon: Lexicon = current_lexicon
if not user.is_authenticated:
flash("You must be a player to view this page")
if lexicon.public:
return redirect(url_for('lexicon.contents', lexicon_name=lexicon.name))
else:
return redirect(url_for('home.home'))
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: if not mem:
flash("You must be a player to view this page") flash("You must be a player to view this page")
@ -134,8 +140,8 @@ def player_required_if_not_public(route):
def player_route(*args, **kwargs): def player_route(*args, **kwargs):
db: DbContext = g.db db: DbContext = g.db
user: User = current_user user: User = current_user
lexicon: Lexicon = g.lexicon lexicon: Lexicon = current_lexicon
if not lexicon.public: if not user.is_authenticated and not lexicon.public:
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: if not mem:
flash("You must be a player to view this page") flash("You must be a player to view this page")
@ -152,7 +158,13 @@ def editor_required(route):
def editor_route(*args, **kwargs): def editor_route(*args, **kwargs):
db: DbContext = g.db db: DbContext = g.db
user: User = current_user user: User = current_user
lexicon: Lexicon = g.lexicon lexicon: Lexicon = current_lexicon
if not user.is_authenticated:
flash("You must be a player to view this page")
if lexicon.public:
return redirect(url_for('lexicon.contents', lexicon_name=lexicon.name))
else:
return redirect(url_for('home.home'))
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")

View File

@ -1,12 +1,8 @@
from operator import le from flask import Blueprint, render_template, g
from sqlalchemy.sql.expression import true from amanuensis.backend import userq, lexiq
from amanuensis.db.models import Lexicon
from flask import Blueprint, render_template, g, url_for, redirect
from amanuensis.backend import userq, lexiq, memq # from .forms import LexiconCreateForm
from .forms import LexiconCreateForm
bp = Blueprint("home", __name__, url_prefix="/home", template_folder=".") bp = Blueprint("home", __name__, url_prefix="/home", template_folder=".")
@ -22,34 +18,6 @@ def home():
def admin(): def admin():
return render_template("home.admin.jinja", userq=userq, lexiq=lexiq) return render_template("home.admin.jinja", userq=userq, lexiq=lexiq)
@bp.get("/admin/create/")
def create():
form = LexiconCreateForm()
return render_template("home.create.jinja", form=form)
@bp.post("/admin/create/")
def create_post():
form = LexiconCreateForm()
if form.validate():
name = form.lexicon.data
editor = userq.try_from_username(g.db, form.editor.data)
prompt = form.prompt.data
assert editor is not None
lexicon = lexiq.create(g.db, name, None, prompt)
new_membership = memq.create(g.db, editor.id, lexicon.id, is_editor=True)
lexicon.joinable = False
g.db.session.commit()
return redirect(url_for("home.admin"))
else:
return render_template("home.create.jinja", form=form)
# @bp_home.route("/admin/create/", methods=['GET', 'POST']) # @bp_home.route("/admin/create/", methods=['GET', 'POST'])
# @login_required # @login_required

View File

@ -2,13 +2,16 @@ from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
class LexiconCreateForm(FlaskForm): # from amanuensis.server.forms import User, Lexicon
"""/admin/create/"""
lexicon = StringField(
'Lexicon name', # class LexiconCreateForm(FlaskForm):
validators=[DataRequired()]) # """/admin/create/"""
editor = StringField( # lexiconName = StringField(
'Username of editor', # 'Lexicon name',
validators=[DataRequired()]) # validators=[DataRequired(), Lexicon(should_exist=False)])
prompt = TextAreaField('Prompt') # editorName = StringField(
submit = SubmitField('Create') # 'Username of editor',
# validators=[DataRequired(), User(should_exist=True)])
# promptText = TextAreaField('Prompt')
# submit = SubmitField('Create')

View File

@ -3,11 +3,13 @@
{% block title %}Admin | Amanuensis{% endblock %} {% block title %}Admin | Amanuensis{% endblock %}
{% block header %}<h2>Amanuensis - Admin Dashboard</h2>{% endblock %} {% block header %}<h2>Amanuensis - Admin Dashboard</h2>{% endblock %}
{# TODO #}
{% block sb_home %}<a href="{{ url_for('home.home') }}">Home</a>{% endblock %} {% block sb_home %}<a href="{{ url_for('home.home') }}">Home</a>{% endblock %}
{% block sb_create %}<a href="{{ url_for('home.create') }}">Create a lexicon</a>{% endblock %} {% block sb_create %}<a href="#{#{ url_for('home.admin_create') }#}">Create a lexicon</a>{% endblock %}
{% set template_sidebar_rows = [self.sb_home(), self.sb_create()] %} {% set template_sidebar_rows = [self.sb_home(), self.sb_create()] %}
{% block main %} {% block main %}
<section>
<p>Users:</p> <p>Users:</p>
{% for user in userq.get_all(db) %} {% for user in userq.get_all(db) %}
{{ macros.dashboard_user_item(user) }} {{ macros.dashboard_user_item(user) }}
@ -16,5 +18,5 @@
{% for lexicon in lexiq.get_all(db) %} {% for lexicon in lexiq.get_all(db) %}
{{ macros.dashboard_lexicon_item(lexicon) }} {{ macros.dashboard_lexicon_item(lexicon) }}
{% endfor %} {% endfor %}
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -8,24 +8,22 @@
{% set template_sidebar_rows = [self.sb_home(), self.sb_admin()] %} {% set template_sidebar_rows = [self.sb_home(), self.sb_admin()] %}
{% block main %} {% block main %}
<form action="" method="post" novalidate> <form action="" method="post" novalidate>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<p>{{ form.lexicon.label }}<br>{{ form.lexicon(size=32) }} <p>{{ form.lexiconName.label }}<br>{{ form.lexiconName(size=32) }}
{% for error in form.lexicon.errors %} {% for error in form.lexiconName.errors %}
<br><span style="color: #ff0000">{{ error }}</span> <br><span style="color: #ff0000">{{ error }}</span>
{% endfor %}</p> {% endfor %}</p>
<p>{{ form.editor.label }}<br>{{ form.editor(size=32) }} <p>{{ form.editorName.label }}<br>{{ form.editorName(size=32) }}
{% for error in form.editor.errors %} {% for error in form.editorName.errors %}
<br><span style="color: #ff0000">{{ error }}</span> <br><span style="color: #ff0000">{{ error }}</span>
{% endfor %}</p> {% endfor %}</p>
<p>{{ form.prompt.label }}<br>{{ form.prompt(class_="fullwidth") }}</p> <p>{{ form.promptText.label }}<br>{{ form.promptText(class_="fullwidth") }}</p>
<p>{{ form.submit() }}</p> <p>{{ form.submit() }}</p>
</form> </form>
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}
<span style="color: #ff0000">{{ message }}</span><br> <span style="color: #ff0000">{{ message }}</span><br>
{% endfor %} {% endfor %}

View File

@ -4,6 +4,13 @@
{% block header %}<h2>Amanuensis - Home</h2>{% endblock %} {% block header %}<h2>Amanuensis - Home</h2>{% endblock %}
{% block main %} {% block main %}
{% if current_user.is_site_admin %}
<section>
<a href="{{ url_for('home.admin') }}" style="display:block; text-align:center;">Admin dashboard</a>
</section>
{% endif %}
<section>
<h1>Welcome to Amanuensis!</h1> <h1>Welcome to Amanuensis!</h1>
<p>Amanuensis is a hub for playing Lexicon, the encyclopedia RPG. Log in to access your Lexicon games. If you do not have an account, contact the administrator.</p> <p>Amanuensis is a hub for playing Lexicon, the encyclopedia RPG. Log in to access your Lexicon games. If you do not have an account, contact the administrator.</p>
@ -37,13 +44,5 @@
{% else %} {% else %}
<p>No public games available.</p> <p>No public games available.</p>
{% endif %} {% endif %}
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}
{% if current_user.is_site_admin %}
{% block admin_dash %}
<a href="{{ url_for('home.admin') }}" style="display:block; text-align:center;">Admin dashboard</a>
{% endblock %}
{% set template_content_blocks = [self.admin_dash()] + template_content_blocks %}
{% endif %}

View File

@ -15,6 +15,10 @@
{% if current_page == "contents" %}class="current-page" {% if current_page == "contents" %}class="current-page"
{% else %}href="{{ url_for('lexicon.contents', lexicon_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_posts %}<a
{% if current_page == "posts" %}class="current-page"
{% else %}href="{{ url_for('lexicon.posts.list', lexicon_name=g.lexicon.name) }}"
{% endif %}>Posts{% set unread_count = postq.get_unread_count(g.db, current_membership.id) if current_membership else None %}{% if unread_count %} ({{ unread_count }}){% endif %}</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', lexicon_name=g.lexicon.name) }}" {% else %}href="{{ url_for('lexicon.rules', lexicon_name=g.lexicon.name) }}"
@ -31,6 +35,7 @@
{% set template_sidebar_rows = [ {% set template_sidebar_rows = [
self.sb_characters(), self.sb_characters(),
self.sb_contents(), self.sb_contents(),
self.sb_posts(),
self.sb_rules(), self.sb_rules(),
self.sb_settings(), self.sb_settings(),
self.sb_stats()] %} self.sb_stats()] %}

View File

@ -8,6 +8,7 @@ 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 .posts import bp as posts_bp
from .settings import bp as settings_bp from .settings import bp as settings_bp
@ -15,6 +16,7 @@ bp = Blueprint(
"lexicon", __name__, url_prefix="/lexicon/<lexicon_name>", template_folder="." "lexicon", __name__, url_prefix="/lexicon/<lexicon_name>", template_folder="."
) )
bp.register_blueprint(characters_bp) bp.register_blueprint(characters_bp)
bp.register_blueprint(posts_bp)
bp.register_blueprint(settings_bp) bp.register_blueprint(settings_bp)

View File

@ -2,6 +2,7 @@
{% block title %}Edit {{ character.name }} | {{ lexicon_title }}{% endblock %} {% block title %}Edit {{ character.name }} | {{ lexicon_title }}{% endblock %}
{% block main %} {% block main %}
<section>
<form action="" method="post" novalidate> <form action="" method="post" novalidate>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<p> <p>
@ -19,6 +20,5 @@
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}
<span style="color:#ff0000">{{ message }}</span><br> <span style="color:#ff0000">{{ message }}</span><br>
{% endfor %} {% endfor %}
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -3,6 +3,7 @@
{% block title %}Character | {{ lexicon_title }}{% endblock %} {% block title %}Character | {{ lexicon_title }}{% endblock %}
{% block main %} {% block main %}
<section>
<h1>Characters</h1> <h1>Characters</h1>
{% set players = memq.get_players_in_lexicon(db, g.lexicon.id)|list %} {% set players = memq.get_players_in_lexicon(db, g.lexicon.id)|list %}
{% set characters = charq.get_in_lexicon(db, g.lexicon.id)|list %} {% set characters = charq.get_in_lexicon(db, g.lexicon.id)|list %}
@ -30,5 +31,5 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -2,17 +2,15 @@
{% block title %}{{ article.title }} | {{ lexicon_title }}{% endblock %} {% block title %}{{ article.title }} | {{ lexicon_title }}{% endblock %}
{% block main %} {% block main %}
<section>
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}
<span style="color:#ff0000">{{ message }}</span><br> <span style="color:#ff0000">{{ message }}</span><br>
{% endfor %} {% endfor %}
<h1>{{ article.title }}</h1> <h1>{{ article.title }}</h1>
{{ article.html }} {{ article.html }}
</section>
{% endblock %} <section>
{% block citations %}
<p> <p>
{% for citation in article.cites %} {% for citation in article.cites %}
<a href="{{ citation|articlelink }}">{{ citation }}</a>{% if not loop.last %} / {% endif %} <a href="{{ citation|articlelink }}">{{ citation }}</a>{% if not loop.last %} / {% endif %}
@ -23,6 +21,5 @@
<a href="{{ citation|articlelink }}">{{ citation }}</a>{% if not loop.last %} / {% endif %} <a href="{{ citation|articlelink }}">{{ citation }}</a>{% if not loop.last %} / {% endif %}
{% endfor %} {% endfor %}
</p> </p>
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main(), self.citations()] %}

View File

@ -3,7 +3,7 @@
{% block title %}Index | {{ lexicon_title }}{% endblock %} {% block title %}Index | {{ lexicon_title }}{% endblock %}
{% block main %} {% block main %}
<section>
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}
<span style="color:#ff0000">{{ message }}</span><br> <span style="color:#ff0000">{{ message }}</span><br>
{% endfor %} {% endfor %}
@ -20,6 +20,5 @@
</ul> </ul>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -2,7 +2,7 @@
{% block title %}Join | {{ g.lexicon.full_title }}{% endblock %} {% block title %}Join | {{ g.lexicon.full_title }}{% endblock %}
{% block main %} {% block main %}
<section>
<form id="lexicon-join" action="" method="post" novalidate> <form id="lexicon-join" action="" method="post" novalidate>
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% if g.lexicon.join_password %} {% if g.lexicon.join_password %}
@ -16,6 +16,5 @@
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}
<span style="color:#ff0000">{{ message }}</span><br> <span style="color:#ff0000">{{ message }}</span><br>
{% endfor %} {% endfor %}
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -3,8 +3,7 @@
{% block title %}Rules | {{ lexicon_title }}{% endblock %} {% block title %}Rules | {{ lexicon_title }}{% endblock %}
{% block main %} {% block main %}
<section>
Placeholder text Placeholder text
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -3,8 +3,7 @@
{% block title %}Session | {{ lexicon_title }}{% endblock %} {% block title %}Session | {{ lexicon_title }}{% endblock %}
{% block main %} {% block main %}
<section>
Placeholder text Placeholder text
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -0,0 +1,95 @@
from flask import Blueprint, render_template, g
from flask.helpers import url_for
from flask_login import current_user
from werkzeug.utils import redirect
from amanuensis.backend import postq
from amanuensis.db import Post
from amanuensis.parser import RenderableVisitor, parse_raw_markdown
from amanuensis.parser.core import *
from amanuensis.server.helpers import (
lexicon_param,
player_required,
current_lexicon,
current_membership,
)
from .forms import CreatePostForm
bp = Blueprint("posts", __name__, url_prefix="/posts", template_folder=".")
class PostFormatter(RenderableVisitor):
"""Parses stylistic markdown into HTML without links."""
def TextSpan(self, span: TextSpan):
return span.innertext
def LineBreak(self, span: LineBreak):
return "<br>"
def ParsedArticle(self, span: ParsedArticle):
return "\n".join(span.recurse(self))
def BodyParagraph(self, span: BodyParagraph):
return f'<p>{"".join(span.recurse(self))}</p>'
def SignatureParagraph(self, span: SignatureParagraph):
return (
'<hr><span class="signature"><p>'
f'{"".join(span.recurse(self))}'
"</p></span>"
)
def BoldSpan(self, span: BoldSpan):
return f'<b>{"".join(span.recurse(self))}</b>'
def ItalicSpan(self, span: ItalicSpan):
return f'<i>{"".join(span.recurse(self))}</i>'
def CitationSpan(self, span: CitationSpan):
return "".join(span.recurse(self))
def render_post_body(post: Post) -> str:
"""Parse and render the body of a post into post-safe HTML."""
renderable: ParsedArticle = parse_raw_markdown(post.body)
rendered: str = renderable.render(PostFormatter())
return rendered
@bp.get("/")
@lexicon_param
@player_required
def list(lexicon_name):
form = CreatePostForm()
new_posts, old_posts = postq.get_posts_for_membership(g.db, current_membership.id)
return render_template(
"posts.jinja",
lexicon_name=lexicon_name,
form=form,
render_post_body=render_post_body,
new_posts=new_posts,
old_posts=old_posts,
)
@bp.post("/")
@lexicon_param
@player_required
def create(lexicon_name):
form = CreatePostForm()
if form.validate():
# Data is valid
postq.create(g.db, current_lexicon.id, current_user.id, form.body.data)
return redirect(url_for("lexicon.posts.list", lexicon_name=lexicon_name))
else:
# POST received invalid data
return render_template(
"posts.jinja",
lexicon_name=lexicon_name,
form=form,
render_post_body=render_post_body,
)

View File

@ -0,0 +1,10 @@
from flask_wtf import FlaskForm
from wtforms import SubmitField, TextAreaField
from wtforms.validators import DataRequired
class CreatePostForm(FlaskForm):
"""/lexicon/<name>/posts/"""
body = TextAreaField(validators=[DataRequired()])
submit = SubmitField("Post")

View File

@ -0,0 +1,28 @@
{% extends "lexicon.jinja" %}
{% set current_page = "posts" %}
{% block title %}Character | {{ lexicon_title }}{% endblock %}
{% macro make_post(post, is_new) %}
<section{% if is_new %} class="new-post"{% endif %}>
<p>{{ render_post_body(post) }}</p>
<p class="post-byline">Posted {% if post.user_id %}by {{ post.user.display_name }} {% endif %}at {{ post.created }}</p>
</section>
{% endmacro %}
{% block main %}
{% if current_lexicon.allow_post %}
<section>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>{{ form.body(class_='fullwidth') }}</p>
<p>{{ form.submit() }}</p>
</form>
</section>
{% endif %}
{% for post in new_posts %}
{{ make_post(post, True) }}
{% endfor %}
{% for post in old_posts %}
{{ make_post(post, False) }}
{% endfor %}
{% endblock %}

View File

@ -89,6 +89,7 @@ def setup(lexicon_name):
form.turn_count.data = lexicon.turn_count form.turn_count.data = lexicon.turn_count
form.player_limit.data = lexicon.player_limit form.player_limit.data = lexicon.player_limit
form.character_limit.data = lexicon.character_limit form.character_limit.data = lexicon.character_limit
form.allow_post.data = lexicon.allow_post
return render_template( return render_template(
"settings.jinja", "settings.jinja",
lexicon_name=lexicon_name, lexicon_name=lexicon_name,
@ -109,6 +110,7 @@ def setup(lexicon_name):
lexicon.turn_count = form.turn_count.data lexicon.turn_count = form.turn_count.data
lexicon.player_limit = form.player_limit.data lexicon.player_limit = form.player_limit.data
lexicon.character_limit = form.character_limit.data lexicon.character_limit = form.character_limit.data
lexicon.allow_post = form.allow_post.data
g.db.session.commit() # TODO refactor into backend g.db.session.commit() # TODO refactor into backend
flash("Settings saved") flash("Settings saved")
return redirect( return redirect(

View File

@ -49,6 +49,7 @@ class SetupSettingsForm(FlaskForm):
widget=NumberInput(), widget=NumberInput(),
validators=[Optional()], validators=[Optional()],
) )
allow_post = BooleanField("Allow players to make posts")
submit = SubmitField("Submit") submit = SubmitField("Submit")

View File

@ -1,4 +1,5 @@
{% extends "lexicon.jinja" %} {% extends "lexicon.jinja" %}
{% set current_page = "settings" %}
{% block title %}Edit | {{ lexicon_title }}{% endblock %} {% block title %}Edit | {{ lexicon_title }}{% endblock %}
{% macro settings_page_link(page, text) -%} {% macro settings_page_link(page, text) -%}
@ -19,6 +20,7 @@
{% endmacro %} {% endmacro %}
{% block main %} {% block main %}
<section>
{% if current_membership.is_editor %} {% if current_membership.is_editor %}
<ul class="unordered-tabs"> <ul class="unordered-tabs">
<li>{{ settings_page_link("player", "Player Settings") }}</li> <li>{{ settings_page_link("player", "Player Settings") }}</li>
@ -78,6 +80,7 @@
<p> <p>
{{ number_setting(form.character_limit) }} {{ number_setting(form.character_limit) }}
</p> </p>
<p>{{ flag_setting(form.allow_post) }}</p>
<p>{{ form.submit() }}</p> <p>{{ form.submit() }}</p>
</form> </form>
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}
@ -172,6 +175,5 @@
{% if page_name == "article" %} {% if page_name == "article" %}
<h3>Article Requirements</h3> <h3>Article Requirements</h3>
{% endif %} {% endif %}
</section>
{% endblock %} {% endblock %}
{% set template_content_blocks = [self.main()] %}

View File

@ -8,8 +8,8 @@
<link rel="stylesheet" href="{{ url_for('static', filename='page.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='page.css') }}">
</head> </head>
<body> <body>
<div id="wrapper"> <main>
<div id="header"> <header>
<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>
@ -24,15 +24,11 @@
{% endif %} {% endif %}
</div> </div>
{% block header %}{% endblock %} {% block header %}{% endblock %}
</div> </header>
{% block sidebar %}{% endblock %} {% block sidebar %}{% endblock %}
<div id="content" class="{% block content_class %}{% endblock %}"> <article class="{% block content_class %}{% endblock %}">
{% if not template_content_blocks %}{% set template_content_blocks = [] %}{% endif %} {% block main %}{% endblock %}
{% if not content_blocks %}{% set content_blocks = [] %}{% endif %} </article>
{% for content_block in template_content_blocks + content_blocks %}<div class="contentblock"> </main>
{{ content_block|safe }}</div>
{% endfor %}
</div>
</div>
</body> </body>
</html> </html>

View File

@ -1,12 +1,12 @@
{% extends "page.jinja" %} {% extends "page.jinja" %}
{% block sidebar %} {% block sidebar %}
<div id="sidebar"> <nav>
{% if not template_sidebar_rows %}{% set template_sidebar_rows = [] %}{% endif %} {% if not template_sidebar_rows %}{% set template_sidebar_rows = [] %}{% endif %}
{% if not sidebar_rows %}{% set sidebar_rows = [] %}{% endif %} {% if not sidebar_rows %}{% set sidebar_rows = [] %}{% endif %}
<table> <table>
{% for row in template_sidebar_rows + sidebar_rows %} {% for row in template_sidebar_rows + sidebar_rows %}
<tr><td>{{ row|safe }}</td></tr>{% endfor %} <tr><td>{{ row|safe }}</td></tr>{% endfor %}
</table> </table>
</div> </nav>
{% endblock %} {% endblock %}
{% block content_class %}content-2col{% endblock %} {% block content_class %}content-2col{% endblock %}

View File

@ -34,7 +34,7 @@
<body> <body>
<div id="wrapper"> <div id="wrapper">
<div id="editor-left" class="column"> <div id="editor-left" class="column">
<div class="contentblock"> <section>
{# Thin header bar #} {# Thin header bar #}
<div id="editor-header"> <div id="editor-header">
{# Header always includes backlink to lexicon #} {# Header always includes backlink to lexicon #}
@ -103,16 +103,16 @@
{# #}{{ article.contents }}{# {# #}{{ article.contents }}{#
#}</textarea> #}</textarea>
{% endif %} {% endif %}
</div> </section>
</div> </div>
<div id="editor-right" class="column"> <div id="editor-right" class="column">
<div id="preview" class="contentblock"> <section id="preview">
<p>This editor requires Javascript to function.</p> <p>This editor requires Javascript to function.</p>
</div> </div>
<div id="preview-citations" class="contentblock"> <section id="preview-citations">
<p>&nbsp;</p> <p>&nbsp;</p>
</div> </div>
<div id="preview-control" class="contentblock"> <section id="preview-control">
<p>&nbsp;</p> <p>&nbsp;</p>
</div> </div>
</div> </div>