Semantic HTML and post feed #23
|
@ -2,19 +2,19 @@
|
|||
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.models import Lexicon
|
||||
from amanuensis.db.models import Lexicon, Membership
|
||||
from amanuensis.errors import ArgumentError, BackendArgumentTypeError
|
||||
|
||||
|
||||
def create(
|
||||
db: DbContext,
|
||||
lexicon_id: int,
|
||||
user_id: int,
|
||||
user_id: Optional[int],
|
||||
body: str,
|
||||
) -> Post:
|
||||
"""
|
||||
|
@ -47,3 +47,57 @@ def create(
|
|||
db.session.add(new_post)
|
||||
db.session.commit()
|
||||
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()
|
||||
|
|
|
@ -8,6 +8,7 @@ import amanuensis.cli.admin
|
|||
import amanuensis.cli.character
|
||||
import amanuensis.cli.index
|
||||
import amanuensis.cli.lexicon
|
||||
import amanuensis.cli.post
|
||||
import amanuensis.cli.user
|
||||
from amanuensis.db import DbContext
|
||||
|
||||
|
@ -113,6 +114,7 @@ def main():
|
|||
add_subcommand(subparsers, amanuensis.cli.character)
|
||||
add_subcommand(subparsers, amanuensis.cli.index)
|
||||
add_subcommand(subparsers, amanuensis.cli.lexicon)
|
||||
add_subcommand(subparsers, amanuensis.cli.post)
|
||||
add_subcommand(subparsers, amanuensis.cli.user)
|
||||
|
||||
# Parse args and perform top-level arg processing
|
||||
|
|
|
@ -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
|
|
@ -251,7 +251,9 @@ class Lexicon(ModelBase):
|
|||
indices = relationship("ArticleIndex", back_populates="lexicon")
|
||||
index_rules = relationship("ArticleIndexRule", 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 #
|
||||
|
|
|
@ -17,7 +17,7 @@ div#editor-left {
|
|||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
div#editor-left div.contentblock {
|
||||
div#editor-left section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 10px 5px 10px 10px;
|
||||
|
@ -48,7 +48,7 @@ textarea#editor-content {
|
|||
div#editor-right {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
div#editor-right div.contentblock {
|
||||
div#editor-right section {
|
||||
margin: 10px 5px 10px 10px;
|
||||
}
|
||||
span.message-warning {
|
||||
|
|
|
@ -8,14 +8,14 @@ body {
|
|||
line-height: 1.4;
|
||||
font-size: 16px;
|
||||
}
|
||||
div#wrapper {
|
||||
main {
|
||||
max-width: 800px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
div#header {
|
||||
header {
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
background-color: #ffffff;
|
||||
|
@ -24,7 +24,7 @@ div#header {
|
|||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
div#header p, div#header h2 {
|
||||
header p, header h2 {
|
||||
margin: 5px;
|
||||
}
|
||||
div#login-status {
|
||||
|
@ -33,7 +33,7 @@ div#login-status {
|
|||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
div#sidebar {
|
||||
nav {
|
||||
width: 200px;
|
||||
float:left;
|
||||
margin:5px;
|
||||
|
@ -46,7 +46,7 @@ div#sidebar {
|
|||
img#logo {
|
||||
max-width: 200px;
|
||||
}
|
||||
div#sidebar table {
|
||||
nav table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
@ -63,32 +63,32 @@ table a {
|
|||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
div#sidebar table a {
|
||||
nav table a {
|
||||
justify-content: center;
|
||||
}
|
||||
table a:hover {
|
||||
background-color: var(--button-hover);
|
||||
}
|
||||
div#sidebar table a.current-page {
|
||||
nav table a.current-page {
|
||||
background-color: var(--button-current);
|
||||
}
|
||||
div#sidebar table a.current-page:hover {
|
||||
nav table a.current-page:hover {
|
||||
background-color: var(--button-current);
|
||||
}
|
||||
div#sidebar table td {
|
||||
nav table td {
|
||||
padding: 0px; margin: 3px 0;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
div#content {
|
||||
article {
|
||||
margin: 5px;
|
||||
}
|
||||
div.content-2col {
|
||||
article.content-2col {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
left: 226px;
|
||||
max-width: 564px;
|
||||
}
|
||||
div.contentblock {
|
||||
section {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 2px 2px 10px #888888;
|
||||
margin-bottom: 5px;
|
||||
|
@ -215,27 +215,35 @@ details.setting-help {
|
|||
#index-definition-table td input[type=number] {
|
||||
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) {
|
||||
div#wrapper {
|
||||
main {
|
||||
padding: 5px;
|
||||
}
|
||||
div#header {
|
||||
header {
|
||||
max-width: 554px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
div#sidebar {
|
||||
nav {
|
||||
max-width: 548px;
|
||||
width: inherit;
|
||||
float: inherit;
|
||||
margin: 10px auto;
|
||||
}
|
||||
div#content{
|
||||
article{
|
||||
margin: 10px auto;
|
||||
}
|
||||
div.content-1col {
|
||||
article.content-1col {
|
||||
max-width: 564px;
|
||||
}
|
||||
div.content-2col {
|
||||
article.content-2col {
|
||||
max-width: 564px;
|
||||
position: static;
|
||||
right: inherit;
|
||||
|
|
|
@ -76,6 +76,7 @@ def get_app(
|
|||
"memq": memq,
|
||||
"charq": charq,
|
||||
"indq": indq,
|
||||
"postq": postq,
|
||||
"current_lexicon": current_lexicon,
|
||||
"current_membership": current_membership
|
||||
}
|
||||
|
|
|
@ -3,21 +3,22 @@
|
|||
{% block header %}<h2>Amanuensis - Login</h2>{% endblock %}
|
||||
{% block login_status_attr %}style="display:none"{% endblock %}
|
||||
{% block main %}
|
||||
<section>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>{{ form.username.label }}<br>{{ form.username(size=32) }}
|
||||
{% for error in form.username.errors %}
|
||||
<br><span style="color: #ff0000">{{ error }}</span>
|
||||
{% endfor %}</p>
|
||||
<p>{{ form.password.label }}<br>{{ form.password(size=32) }}
|
||||
{% for error in form.password.errors %}
|
||||
<br><span style="color: #ff0000">{{ error }}</span>
|
||||
{% endfor %}</p>
|
||||
<p>{{ form.remember }} {{ form.remember.label }}</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>{{ form.username.label }}<br>{{ form.username(size=32) }}
|
||||
{% for error in form.username.errors %}
|
||||
<br><span style="color: #ff0000">{{ error }}</span>
|
||||
{% endfor %}</p>
|
||||
<p>{{ form.password.label }}<br>{{ form.password(size=32) }}
|
||||
{% for error in form.password.errors %}
|
||||
<br><span style="color: #ff0000">{{ error }}</span>
|
||||
{% endfor %}</p>
|
||||
<p>{{ form.remember }} {{ form.remember.label }}</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
</form>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<span style="color: #ff0000">{{ message }}</span><br>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% set template_content_blocks = [self.main()] %}
|
|
@ -99,7 +99,7 @@ def admin_required(route):
|
|||
@wraps(route)
|
||||
def admin_route(*args, **kwargs):
|
||||
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")
|
||||
return redirect(url_for('home.home'))
|
||||
return route(*args, **kwargs)
|
||||
|
@ -114,7 +114,13 @@ def player_required(route):
|
|||
def player_route(*args, **kwargs):
|
||||
db: DbContext = g.db
|
||||
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)
|
||||
if not mem:
|
||||
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):
|
||||
db: DbContext = g.db
|
||||
user: User = current_user
|
||||
lexicon: Lexicon = g.lexicon
|
||||
if not lexicon.public:
|
||||
lexicon: Lexicon = current_lexicon
|
||||
if not user.is_authenticated and 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")
|
||||
|
@ -152,7 +158,13 @@ def editor_required(route):
|
|||
def editor_route(*args, **kwargs):
|
||||
db: DbContext = g.db
|
||||
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)
|
||||
if not mem or not mem.is_editor:
|
||||
flash("You must be the editor to view this page")
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
{% set template_sidebar_rows = [self.sb_home(), self.sb_create()] %}
|
||||
|
||||
{% block main %}
|
||||
<section>
|
||||
<p>Users:</p>
|
||||
{% for user in userq.get_all(db) %}
|
||||
{{ macros.dashboard_user_item(user) }}
|
||||
|
@ -17,5 +18,5 @@
|
|||
{% for lexicon in lexiq.get_all(db) %}
|
||||
{{ macros.dashboard_lexicon_item(lexicon) }}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% set template_content_blocks = [self.main()] %}
|
|
@ -4,6 +4,13 @@
|
|||
{% block header %}<h2>Amanuensis - Home</h2>{% endblock %}
|
||||
|
||||
{% 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>
|
||||
<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 %}
|
||||
<p>No public games available.</p>
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
{% 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 %}
|
|
@ -15,6 +15,10 @@
|
|||
{% if current_page == "contents" %}class="current-page"
|
||||
{% else %}href="{{ url_for('lexicon.contents', lexicon_name=g.lexicon.name) }}"
|
||||
{% 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
|
||||
{% if current_page == "rules" %}class="current-page"
|
||||
{% else %}href="{{ url_for('lexicon.rules', lexicon_name=g.lexicon.name) }}"
|
||||
|
@ -31,6 +35,7 @@
|
|||
{% set template_sidebar_rows = [
|
||||
self.sb_characters(),
|
||||
self.sb_contents(),
|
||||
self.sb_posts(),
|
||||
self.sb_rules(),
|
||||
self.sb_settings(),
|
||||
self.sb_stats()] %}
|
||||
|
|
|
@ -8,6 +8,7 @@ from amanuensis.server.helpers import lexicon_param, player_required_if_not_publ
|
|||
|
||||
from .characters import bp as characters_bp
|
||||
from .forms import LexiconJoinForm
|
||||
from .posts import bp as posts_bp
|
||||
from .settings import bp as settings_bp
|
||||
|
||||
|
||||
|
@ -15,6 +16,7 @@ bp = Blueprint(
|
|||
"lexicon", __name__, url_prefix="/lexicon/<lexicon_name>", template_folder="."
|
||||
)
|
||||
bp.register_blueprint(characters_bp)
|
||||
bp.register_blueprint(posts_bp)
|
||||
bp.register_blueprint(settings_bp)
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
{% block title %}Edit {{ character.name }} | {{ lexicon_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<section>
|
||||
<form action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<p>
|
||||
|
@ -19,6 +20,5 @@
|
|||
{% for message in get_flashed_messages() %}
|
||||
<span style="color:#ff0000">{{ message }}</span><br>
|
||||
{% endfor %}
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% set template_content_blocks = [self.main()] %}
|
|
@ -3,6 +3,7 @@
|
|||
{% block title %}Character | {{ lexicon_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<section>
|
||||
<h1>Characters</h1>
|
||||
{% set players = memq.get_players_in_lexicon(db, g.lexicon.id)|list %}
|
||||
{% set characters = charq.get_in_lexicon(db, g.lexicon.id)|list %}
|
||||
|
@ -30,5 +31,5 @@
|
|||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% set template_content_blocks = [self.main()] %}
|
|
@ -2,17 +2,15 @@
|
|||
{% block title %}{{ article.title }} | {{ lexicon_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<section>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<span style="color:#ff0000">{{ message }}</span><br>
|
||||
{% endfor %}
|
||||
|
||||
<h1>{{ article.title }}</h1>
|
||||
{{ article.html }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block citations %}
|
||||
</section>
|
||||
<section>
|
||||
<p>
|
||||
{% for citation in article.cites %}
|
||||
<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 %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% set template_content_blocks = [self.main(), self.citations()] %}
|
|
@ -3,7 +3,7 @@
|
|||
{% block title %}Index | {{ lexicon_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<section>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<span style="color:#ff0000">{{ message }}</span><br>
|
||||
{% endfor %}
|
||||
|
@ -20,6 +20,5 @@
|
|||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% set template_content_blocks = [self.main()] %}
|
|
@ -2,7 +2,7 @@
|
|||
{% block title %}Join | {{ g.lexicon.full_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<section>
|
||||
<form id="lexicon-join" action="" method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
{% if g.lexicon.join_password %}
|
||||
|
@ -16,6 +16,5 @@
|
|||
{% for message in get_flashed_messages() %}
|
||||
<span style="color:#ff0000">{{ message }}</span><br>
|
||||
{% endfor %}
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% set template_content_blocks = [self.main()] %}
|
|
@ -3,8 +3,7 @@
|
|||
{% block title %}Rules | {{ lexicon_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<section>
|
||||
Placeholder text
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% set template_content_blocks = [self.main()] %}
|
|
@ -3,8 +3,7 @@
|
|||
{% block title %}Session | {{ lexicon_title }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<section>
|
||||
Placeholder text
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% set template_content_blocks = [self.main()] %}
|
|
@ -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,
|
||||
)
|
|
@ -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")
|
|
@ -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 %}
|
|
@ -89,6 +89,7 @@ def setup(lexicon_name):
|
|||
form.turn_count.data = lexicon.turn_count
|
||||
form.player_limit.data = lexicon.player_limit
|
||||
form.character_limit.data = lexicon.character_limit
|
||||
form.allow_post.data = lexicon.allow_post
|
||||
return render_template(
|
||||
"settings.jinja",
|
||||
lexicon_name=lexicon_name,
|
||||
|
@ -109,6 +110,7 @@ def setup(lexicon_name):
|
|||
lexicon.turn_count = form.turn_count.data
|
||||
lexicon.player_limit = form.player_limit.data
|
||||
lexicon.character_limit = form.character_limit.data
|
||||
lexicon.allow_post = form.allow_post.data
|
||||
g.db.session.commit() # TODO refactor into backend
|
||||
flash("Settings saved")
|
||||
return redirect(
|
||||
|
|
|
@ -49,6 +49,7 @@ class SetupSettingsForm(FlaskForm):
|
|||
widget=NumberInput(),
|
||||
validators=[Optional()],
|
||||
)
|
||||
allow_post = BooleanField("Allow players to make posts")
|
||||
submit = SubmitField("Submit")
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "lexicon.jinja" %}
|
||||
{% set current_page = "settings" %}
|
||||
{% block title %}Edit | {{ lexicon_title }}{% endblock %}
|
||||
|
||||
{% macro settings_page_link(page, text) -%}
|
||||
|
@ -19,6 +20,7 @@
|
|||
{% endmacro %}
|
||||
|
||||
{% block main %}
|
||||
<section>
|
||||
{% if current_membership.is_editor %}
|
||||
<ul class="unordered-tabs">
|
||||
<li>{{ settings_page_link("player", "Player Settings") }}</li>
|
||||
|
@ -78,6 +80,7 @@
|
|||
<p>
|
||||
{{ number_setting(form.character_limit) }}
|
||||
</p>
|
||||
<p>{{ flag_setting(form.allow_post) }}</p>
|
||||
<p>{{ form.submit() }}</p>
|
||||
</form>
|
||||
{% for message in get_flashed_messages() %}
|
||||
|
@ -172,6 +175,5 @@
|
|||
{% if page_name == "article" %}
|
||||
<h3>Article Requirements</h3>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% set template_content_blocks = [self.main()] %}
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
<link rel="stylesheet" href="{{ url_for('static', filename='page.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<div id="header">
|
||||
<main>
|
||||
<header>
|
||||
<div id="login-status" {% block login_status_attr %}{% endblock %}>
|
||||
{% if current_user.is_authenticated %}
|
||||
<b>{{ current_user.username -}}</b>
|
||||
|
@ -24,15 +24,11 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
{% block header %}{% endblock %}
|
||||
</div>
|
||||
</header>
|
||||
{% block sidebar %}{% endblock %}
|
||||
<div id="content" class="{% block content_class %}{% endblock %}">
|
||||
{% if not template_content_blocks %}{% set template_content_blocks = [] %}{% endif %}
|
||||
{% if not content_blocks %}{% set content_blocks = [] %}{% endif %}
|
||||
{% for content_block in template_content_blocks + content_blocks %}<div class="contentblock">
|
||||
{{ content_block|safe }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<article class="{% block content_class %}{% endblock %}">
|
||||
{% block main %}{% endblock %}
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{% extends "page.jinja" %}
|
||||
{% block sidebar %}
|
||||
<div id="sidebar">
|
||||
<nav>
|
||||
{% if not template_sidebar_rows %}{% set template_sidebar_rows = [] %}{% endif %}
|
||||
{% if not sidebar_rows %}{% set sidebar_rows = [] %}{% endif %}
|
||||
<table>
|
||||
{% for row in template_sidebar_rows + sidebar_rows %}
|
||||
<tr><td>{{ row|safe }}</td></tr>{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
{% block content_class %}content-2col{% endblock %}
|
|
@ -34,7 +34,7 @@
|
|||
<body>
|
||||
<div id="wrapper">
|
||||
<div id="editor-left" class="column">
|
||||
<div class="contentblock">
|
||||
<section>
|
||||
{# Thin header bar #}
|
||||
<div id="editor-header">
|
||||
{# Header always includes backlink to lexicon #}
|
||||
|
@ -103,16 +103,16 @@
|
|||
{# #}{{ article.contents }}{#
|
||||
#}</textarea>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div id="editor-right" class="column">
|
||||
<div id="preview" class="contentblock">
|
||||
<section id="preview">
|
||||
<p>This editor requires Javascript to function.</p>
|
||||
</div>
|
||||
<div id="preview-citations" class="contentblock">
|
||||
<section id="preview-citations">
|
||||
<p> </p>
|
||||
</div>
|
||||
<div id="preview-control" class="contentblock">
|
||||
<section id="preview-control">
|
||||
<p> </p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue