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() ).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.""" """Update an article."""
db( db(
update(Article) update(Article)

View File

@ -1,6 +1,7 @@
import re import re
from typing import Sequence from typing import Sequence
from amanuensis.db import *
from amanuensis.parser import * from amanuensis.parser import *
@ -9,18 +10,14 @@ class ConstraintCheck(RenderableVisitor):
def __init__(self) -> None: def __init__(self) -> None:
self.word_count: int = 0 self.word_count: int = 0
self.signatures: int = 0 self.signatures: int = 0
self.tmp: bool = False
def TextSpan(self, span): def TextSpan(self, span):
self.word_count += len(re.split(r'\s+', span.innertext.strip())) self.word_count += len(re.split(r'\s+', span.innertext.strip()))
return self return self
def BoldSpan(self, span):
self.tmp = True
return self
def SignatureParagraph(self, span): def SignatureParagraph(self, span):
self.signatures += 1 self.signatures += 1
span.recurse(self)
return self return self
@ -53,21 +50,102 @@ class ConstraintMessage:
return {"severity": self.severity, "message": self.message} 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()) check_result: ConstraintCheck = parsed.render(ConstraintCheck())
messages = [] messages = []
# I: Word count # I: Word count
messages.append(ConstraintMessage.info(f"Word count: {check_result.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: if check_result.signatures < 1:
messages.append(ConstraintMessage.warning("Missing signature paragraph")) messages.append(ConstraintMessage.warning("Missing signature paragraph"))
if check_result.signatures > 1: if check_result.signatures > 1:
messages.append(ConstraintMessage.warning("More than one signature paragraph")) 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 return messages

View File

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