Compare commits

..

1 Commits

Author SHA1 Message Date
Tim Van Baak 2686a9cf22 Implement draft previews and constraint analysis 2021-10-13 22:12:40 -07:00
3 changed files with 125 additions and 39 deletions

View File

@ -54,7 +54,9 @@ def try_from_public_id(db: DbContext, public_id: UUID) -> Optional[Article]:
).scalar_one_or_none()
def update_state(db: DbContext, article_id: int, title: str, body: str, state: ArticleState):
def update_state(
db: DbContext, article_id: int, title: str, body: str, state: ArticleState
):
"""Update an article."""
db(
update(Article)

View File

@ -1,6 +1,7 @@
import re
from typing import Sequence
from amanuensis.db import *
from amanuensis.parser import *
@ -9,18 +10,14 @@ class ConstraintCheck(RenderableVisitor):
def __init__(self) -> None:
self.word_count: int = 0
self.signatures: int = 0
self.tmp: bool = False
def TextSpan(self, span):
self.word_count += len(re.split(r'\s+', span.innertext.strip()))
return self
def BoldSpan(self, span):
self.tmp = True
return self
def SignatureParagraph(self, span):
self.signatures += 1
span.recurse(self)
return self
@ -53,21 +50,102 @@ class ConstraintMessage:
return {"severity": self.severity, "message": self.message}
def constraint_check(parsed: Renderable) -> Sequence[ConstraintMessage]:
def title_constraint_check(title: str) -> Sequence[ConstraintMessage]:
"""Perform checks that apply to the article title."""
messages = []
# I: Current index assignments
# TODO
# E: No title
if not title:
messages.append(ConstraintMessage.error("Missing title"))
# I: This article is new
# TODO
# E: And new articles are forbidden
# TODO
# I: This article is a phantom
# TODO
# I: This article is an addendum
# TODO
# E: And the user is at the addendum limit
# TODO
# E: This article has already been written and addendums are disabled
# TODO
# I: This article's index
# TODO
# E: The article does not fulfill the player's index assignment
# TODO
# E: The index does not have room for a new article
# TODO
# E: The player has cited this phantom article before
# TODO
# W: Another player is writing an article with this title
# TODO
# E: Another player has an approved article with this title
# TODO
# W: The article's title matches a character's name
# TODO
return messages
def content_constraint_check(parsed: Renderable) -> Sequence[ConstraintMessage]:
check_result: ConstraintCheck = parsed.render(ConstraintCheck())
messages = []
# I: Word count
messages.append(ConstraintMessage.info(f"Word count: {check_result.word_count}"))
# W: Check signature count
# E: Self-citation when forbidden
# TODO
# W: A new citation matches a character's name
# TODO
# W: The article cites itself
# TODO
# E: A new citation would create more articles than can be written
# TODO
# E: Extant citation count requirements
# TODO
# E: Phantom citation count requirements
# TODO
# E: New citation count requirements
# TODO
# E: Character citation count requirements
# TODO
# E: Total citation count requirements
# TODO
# E: Exceeded hard word limit
# TODO
# W: Exceeded soft word limit
# TODO
# W: Missing or multiple signatures
if check_result.signatures < 1:
messages.append(ConstraintMessage.warning("Missing signature paragraph"))
if check_result.signatures > 1:
messages.append(ConstraintMessage.warning("More than one signature paragraph"))
# E: tmp to test errors
if check_result.tmp:
messages.append(ConstraintMessage.error("Temporary error"))
return messages

View File

@ -4,7 +4,7 @@ from flask import Blueprint, render_template, g, abort, request
from amanuensis.backend import *
from amanuensis.db import *
from amanuensis.lexicon.constraint import constraint_check
from amanuensis.lexicon.constraint import title_constraint_check, content_constraint_check
from amanuensis.parser import *
from amanuensis.server.helpers import lexicon_param, player_required
@ -14,6 +14,7 @@ bp = Blueprint("editor", __name__, url_prefix="/editor", template_folder=".")
class PreviewHtmlRenderer(RenderableVisitor):
"""Parses stylistic markdown and stores citations as footnotes."""
def __init__(self) -> None:
self.citations: list = []
self.rendered: str = ""
@ -23,7 +24,7 @@ class PreviewHtmlRenderer(RenderableVisitor):
return span.innertext
def LineBreak(self, span):
return '<br>'
return "<br>"
# Translate the simple container spans to text
def BoldSpan(self, span):
@ -35,10 +36,7 @@ class PreviewHtmlRenderer(RenderableVisitor):
# Record citations in the visitor, then translate the span to text as an
# underline and footnote number
def CitationSpan(self, span):
self.citations.append({
"title": span.cite_target,
"type": "phantom"
})
self.citations.append({"title": span.cite_target, "type": "phantom"})
return f'<u>{"".join(span.recurse(self))}</u>[{len(self.citations)}]'
# Translate the paragraph-level containers to their text contents
@ -49,13 +47,13 @@ class PreviewHtmlRenderer(RenderableVisitor):
return (
'<hr><span class="signature"><p>'
f'{"".join(span.recurse(self))}'
'</p></span>'
"</p></span>"
)
# Return the visitor from the top-level article span after saving the full
# text parsed from the child spans
def ParsedArticle(self, span):
self.contents = '\n'.join(span.recurse(self))
self.contents = "\n".join(span.recurse(self))
return self
@ -94,17 +92,18 @@ def load(lexicon_name, article_id):
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
# Check article content against constraints
messages = constraint_check(parsed)
messages = title_constraint_check(article.title)
messages.extend(content_constraint_check(parsed))
# Return the article information to the editor
msg_list = list([msg.json() for msg in messages])
return {
'title': article.title,
'rendered': preview_result.contents,
'state': article.state.value,
'ersatz': article.ersatz,
'citations': preview_result.citations,
'messages': msg_list,
"title": article.title,
"rendered": preview_result.contents,
"state": article.state.value,
"ersatz": article.ersatz,
"citations": preview_result.citations,
"messages": msg_list,
}
@ -116,33 +115,40 @@ def update(lexicon_name, article_id):
return abort(404)
# Extract the submitted content
new_title = request.json['title']
new_body = request.json['body']
new_state = ArticleState(request.json['state'])
new_title = request.json["title"]
new_body = request.json["body"]
new_state = ArticleState(request.json["state"])
# Generate the preview HTML from the submitted content
parsed = parse_raw_markdown(new_body)
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
# Check article content against constraints
messages = constraint_check(parsed)
messages = title_constraint_check(new_title)
messages.extend(content_constraint_check(parsed))
# Block submission if the article is a draft with errors
has_errors = any([msg for msg in messages if msg.is_error])
if article.state == ArticleState.DRAFT and new_state != ArticleState.DRAFT and has_errors:
if (
article.state == ArticleState.DRAFT
and new_state != ArticleState.DRAFT
and has_errors
):
new_state = ArticleState.DRAFT
# Update the article with the submitted information
artiq.update_state(g.db, article.id, title=new_title, body=new_body, state=new_state)
artiq.update_state(
g.db, article.id, title=new_title, body=new_body, state=new_state
)
updated_article = artiq.try_from_public_id(g.db, article_id)
# Return the article information to the editor
msg_list = list([msg.json() for msg in messages])
return {
'title': updated_article.title,
'rendered': preview_result.contents,
'state': updated_article.state.value,
'ersatz': updated_article.ersatz,
'citations': preview_result.citations,
'messages': msg_list,
"title": updated_article.title,
"rendered": preview_result.contents,
"state": updated_article.state.value,
"ersatz": updated_article.ersatz,
"citations": preview_result.citations,
"messages": msg_list,
}