diff --git a/amanuensis/backend/post.py b/amanuensis/backend/post.py index 8a4d59d..6c0913b 100644 --- a/amanuensis/backend/post.py +++ b/amanuensis/backend/post.py @@ -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() diff --git a/amanuensis/cli/__init__.py b/amanuensis/cli/__init__.py index e457db4..56af06e 100644 --- a/amanuensis/cli/__init__.py +++ b/amanuensis/cli/__init__.py @@ -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 diff --git a/amanuensis/cli/post.py b/amanuensis/cli/post.py new file mode 100644 index 0000000..1dfd7f7 --- /dev/null +++ b/amanuensis/cli/post.py @@ -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 diff --git a/amanuensis/db/models.py b/amanuensis/db/models.py index e951309..0f7aead 100644 --- a/amanuensis/db/models.py +++ b/amanuensis/db/models.py @@ -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 # diff --git a/amanuensis/resources/editor.css b/amanuensis/resources/editor.css index fb0e52b..2ccbbaa 100644 --- a/amanuensis/resources/editor.css +++ b/amanuensis/resources/editor.css @@ -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 { diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index bffd77b..0a9149d 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -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; diff --git a/amanuensis/server/__init__.py b/amanuensis/server/__init__.py index c8a9827..4f70c90 100644 --- a/amanuensis/server/__init__.py +++ b/amanuensis/server/__init__.py @@ -76,6 +76,7 @@ def get_app( "memq": memq, "charq": charq, "indq": indq, + "postq": postq, "current_lexicon": current_lexicon, "current_membership": current_membership } diff --git a/amanuensis/server/auth/auth.login.jinja b/amanuensis/server/auth/auth.login.jinja index f6b6ae2..a06dce7 100644 --- a/amanuensis/server/auth/auth.login.jinja +++ b/amanuensis/server/auth/auth.login.jinja @@ -3,21 +3,22 @@ {% block header %}
Users:
{% 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 %} +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.
@@ -37,13 +44,5 @@ {% else %}No public games available.
{% endif %} - +