Semantic HTML and post feed #23

Merged
Jaculabilis merged 10 commits from tvb/post-feed into develop 2021-10-02 00:18:32 +00:00
29 changed files with 340 additions and 94 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

@ -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,6 +3,7 @@
{% 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) }}
@ -19,5 +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

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

@ -9,6 +9,7 @@
{% 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) }}
@ -17,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

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