Compare commits
10 Commits
nvb/create
...
develop
Author | SHA1 | Date |
---|---|---|
Tim Van Baak | 8754823556 | |
Tim Van Baak | d172848231 | |
Tim Van Baak | f88ee3a526 | |
Tim Van Baak | ab2f0edcb1 | |
Tim Van Baak | 19e7af7e58 | |
Tim Van Baak | 3cc487f94b | |
Tim Van Baak | b749357657 | |
Tim Van Baak | 1ca46d47f5 | |
Tim Van Baak | a6399e7e22 | |
Tim Van Baak | ffaf881707 |
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
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 #
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()] %}
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()] %}
|
|
|
@ -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 %}
|
|
|
@ -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()] %}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()] %}
|
|
|
@ -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()] %}
|
|
|
@ -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()] %}
|
|
|
@ -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()] %}
|
|
|
@ -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()] %}
|
|
|
@ -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()] %}
|
|
|
@ -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()] %}
|
|
|
@ -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.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(
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()] %}
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
|
@ -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> </p>
|
<p> </p>
|
||||||
</div>
|
</div>
|
||||||
<div id="preview-control" class="contentblock">
|
<section id="preview-control">
|
||||||
<p> </p>
|
<p> </p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue