Compare commits

..

10 Commits

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

View File

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

View File

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

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")
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 #

View File

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

View File

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

View File

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

View File

@ -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()] %}

View File

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

View File

@ -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()] %}

View File

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

View File

@ -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()] %}

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 .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)

View File

@ -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()] %}

View File

@ -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()] %}

View File

@ -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()] %}

View File

@ -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()] %}

View File

@ -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()] %}

View File

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

View File

@ -3,8 +3,7 @@
{% block title %}Session | {{ lexicon_title }}{% endblock %}
{% block main %}
<section>
Placeholder text
</section>
{% 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.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(

View File

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

View File

@ -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()] %}

View File

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

View File

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

View File

@ -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>&nbsp;</p>
</div>
<div id="preview-control" class="contentblock">
<section id="preview-control">
<p>&nbsp;</p>
</div>
</div>