Compare commits
2 Commits
837ce735aa
...
6aadbaaf0c
Author | SHA1 | Date |
---|---|---|
Tim Van Baak | 6aadbaaf0c | |
Tim Van Baak | 675e42cfa3 |
|
@ -5,7 +5,7 @@ Article query interface
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, update
|
||||||
|
|
||||||
from amanuensis.db import *
|
from amanuensis.db import *
|
||||||
from amanuensis.errors import ArgumentError, BackendArgumentTypeError
|
from amanuensis.errors import ArgumentError, BackendArgumentTypeError
|
||||||
|
@ -52,3 +52,13 @@ def try_from_public_id(db: DbContext, public_id: UUID) -> Optional[Article]:
|
||||||
return db(
|
return db(
|
||||||
select(Article).where(Article.public_id == public_id)
|
select(Article).where(Article.public_id == public_id)
|
||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
def update_state(db: DbContext, article_id: int, title: str, body: str, state: ArticleState):
|
||||||
|
"""Update an article."""
|
||||||
|
db(
|
||||||
|
update(Article)
|
||||||
|
.where(Article.id == article_id)
|
||||||
|
.values(title=title, body=body, state=state)
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import re
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from amanuensis.parser import *
|
||||||
|
|
||||||
|
|
||||||
|
class ConstraintCheck(RenderableVisitor):
|
||||||
|
"""Analyzes an article for content-based constraint violations."""
|
||||||
|
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
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class ConstraintMessage:
|
||||||
|
INFO = 0
|
||||||
|
WARNING = 1
|
||||||
|
ERROR = 2
|
||||||
|
|
||||||
|
def __init__(self, severity: int, message: str) -> None:
|
||||||
|
self.severity = severity
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def info(message) -> "ConstraintMessage":
|
||||||
|
return ConstraintMessage(ConstraintMessage.INFO, message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def warning(message) -> "ConstraintMessage":
|
||||||
|
return ConstraintMessage(ConstraintMessage.WARNING, message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def error(message) -> "ConstraintMessage":
|
||||||
|
return ConstraintMessage(ConstraintMessage.ERROR, message)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_error(self) -> bool:
|
||||||
|
return self.severity == ConstraintMessage.ERROR
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {"severity": self.severity, "message": self.message}
|
||||||
|
|
||||||
|
|
||||||
|
def 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
|
||||||
|
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
|
|
@ -2,11 +2,12 @@
|
||||||
Module encapsulating all markdown parsing functionality.
|
Module encapsulating all markdown parsing functionality.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .core import RenderableVisitor
|
from .core import RenderableVisitor, Renderable
|
||||||
from .helpers import normalize_title, filesafe_title, titlesort
|
from .helpers import normalize_title, filesafe_title, titlesort
|
||||||
from .parsing import parse_raw_markdown
|
from .parsing import parse_raw_markdown
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"Renderable",
|
||||||
"RenderableVisitor",
|
"RenderableVisitor",
|
||||||
"normalize_title",
|
"normalize_title",
|
||||||
"filesafe_title",
|
"filesafe_title",
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
title: undefined,
|
title: undefined,
|
||||||
rendered: undefined,
|
rendered: undefined,
|
||||||
citations: [],
|
citations: [],
|
||||||
errors: []
|
messages: []
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The nonce of the last-made update request. */
|
/** The nonce of the last-made update request. */
|
||||||
|
@ -33,13 +33,11 @@
|
||||||
// Stake a claim on the nonce, potentially overwriting a previous
|
// Stake a claim on the nonce, potentially overwriting a previous
|
||||||
// nonce value.
|
// nonce value.
|
||||||
const nonce_local = 1 + Math.random();
|
const nonce_local = 1 + Math.random();
|
||||||
console.log("ifNoFurtherChanges: timeout=" + timeout.toString() + ", nonce=" + nonce_local.toString());
|
|
||||||
nonce = nonce_local;
|
nonce = nonce_local;
|
||||||
// Wait to see if this call is overwritten in turn.
|
// Wait to see if this call is overwritten in turn.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (nonce == nonce_local)
|
if (nonce == nonce_local)
|
||||||
{
|
{
|
||||||
console.log("Executing for: " + nonce.toString());
|
|
||||||
callback();
|
callback();
|
||||||
nonce = 0;
|
nonce = 0;
|
||||||
}
|
}
|
||||||
|
@ -51,7 +49,7 @@
|
||||||
{
|
{
|
||||||
// Enable or disable controls
|
// Enable or disable controls
|
||||||
const isEditable = article.state == ArticleState.DRAFT;
|
const isEditable = article.state == ArticleState.DRAFT;
|
||||||
const blocked = preview.errors.filter(err => err.severity == 2).length > 0;
|
const blocked = preview.messages.filter(msg => msg.severity == 2).length > 0;
|
||||||
document.getElementById("editor-title").disabled = !isEditable;
|
document.getElementById("editor-title").disabled = !isEditable;
|
||||||
document.getElementById("editor-content").disabled = !isEditable;
|
document.getElementById("editor-content").disabled = !isEditable;
|
||||||
document.getElementById("button-submit").innerText = isEditable ? "Submit article" : "Edit article";
|
document.getElementById("button-submit").innerText = isEditable ? "Submit article" : "Edit article";
|
||||||
|
@ -69,7 +67,7 @@
|
||||||
|
|
||||||
// Fill in the status message block
|
// Fill in the status message block
|
||||||
let statuses = "<ol>";
|
let statuses = "<ol>";
|
||||||
preview.errors.forEach(err => statuses += "<li>" + JSON.stringify(err) + "</li>");
|
preview.messages.forEach(err => statuses += "<li>" + JSON.stringify(err) + "</li>");
|
||||||
statuses += "<ol>";
|
statuses += "<ol>";
|
||||||
document.getElementById("preview-control").innerHTML = statuses;
|
document.getElementById("preview-control").innerHTML = statuses;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +80,7 @@
|
||||||
preview.title = data.title;
|
preview.title = data.title;
|
||||||
preview.rendered = data.rendered;
|
preview.rendered = data.rendered;
|
||||||
preview.citations = data.citations;
|
preview.citations = data.citations;
|
||||||
preview.errors = data.errors;
|
preview.messages = data.messages;
|
||||||
refreshEditor();
|
refreshEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ 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.parser import *
|
from amanuensis.parser import *
|
||||||
from amanuensis.parser.core import Renderable
|
|
||||||
from amanuensis.server.helpers import lexicon_param, player_required
|
from amanuensis.server.helpers import lexicon_param, player_required
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,51 +59,6 @@ class PreviewHtmlRenderer(RenderableVisitor):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class ConstraintCheck(RenderableVisitor):
|
|
||||||
"""Analyzes an article for content-based constraint violations."""
|
|
||||||
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
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
def constraint_check(parsed: Renderable):
|
|
||||||
check_result: ConstraintCheck = parsed.render(ConstraintCheck())
|
|
||||||
errors = []
|
|
||||||
errors.append({
|
|
||||||
"severity": 0,
|
|
||||||
"message": f"Word count: {check_result.word_count}"
|
|
||||||
})
|
|
||||||
if check_result.signatures < 1:
|
|
||||||
errors.append({
|
|
||||||
"severity": 1,
|
|
||||||
"message": "Missing signature paragraph"
|
|
||||||
})
|
|
||||||
if check_result.signatures > 1:
|
|
||||||
errors.append({
|
|
||||||
"severity": 1,
|
|
||||||
"message": "More than one signature paragraph"
|
|
||||||
})
|
|
||||||
if check_result.tmp:
|
|
||||||
errors.append({
|
|
||||||
"severity": 2,
|
|
||||||
"message": "Temporary error"
|
|
||||||
})
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.get("/")
|
||||||
@lexicon_param
|
@lexicon_param
|
||||||
@player_required
|
@player_required
|
||||||
|
@ -129,39 +84,65 @@ def open(lexicon_name, article_id):
|
||||||
@lexicon_param
|
@lexicon_param
|
||||||
@player_required
|
@player_required
|
||||||
def load(lexicon_name, article_id):
|
def load(lexicon_name, article_id):
|
||||||
|
# Get the article
|
||||||
article = artiq.try_from_public_id(g.db, article_id)
|
article = artiq.try_from_public_id(g.db, article_id)
|
||||||
if not article:
|
if not article:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
|
||||||
|
# Generate the preview HTML
|
||||||
parsed = parse_raw_markdown(article.body)
|
parsed = parse_raw_markdown(article.body)
|
||||||
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
|
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
|
||||||
errors = constraint_check(parsed)
|
|
||||||
|
# Check article content against constraints
|
||||||
|
messages = constraint_check(parsed)
|
||||||
|
|
||||||
|
# Return the article information to the editor
|
||||||
|
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,
|
||||||
'errors': errors,
|
'messages': msg_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<uuid:article_id>/update")
|
@bp.post("/<uuid:article_id>/update")
|
||||||
def update(lexicon_name, article_id):
|
def update(lexicon_name, article_id):
|
||||||
|
# Get the article
|
||||||
article = artiq.try_from_public_id(g.db, article_id)
|
article = artiq.try_from_public_id(g.db, article_id)
|
||||||
if not article:
|
if not article:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
article.title = request.json['title']
|
|
||||||
article.body = request.json['body']
|
# Extract the submitted content
|
||||||
article.state = ArticleState(request.json['state'])
|
new_title = request.json['title']
|
||||||
g.db.session.commit()
|
new_body = request.json['body']
|
||||||
parsed = parse_raw_markdown(article.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())
|
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
|
||||||
errors = constraint_check(parsed)
|
|
||||||
|
# Check article content against constraints
|
||||||
|
messages = 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:
|
||||||
|
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)
|
||||||
|
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 {
|
return {
|
||||||
'title': article.title,
|
'title': updated_article.title,
|
||||||
'rendered': preview_result.contents,
|
'rendered': preview_result.contents,
|
||||||
'state': article.state.value,
|
'state': updated_article.state.value,
|
||||||
'ersatz': article.ersatz,
|
'ersatz': updated_article.ersatz,
|
||||||
'citations': preview_result.citations,
|
'citations': preview_result.citations,
|
||||||
'errors': errors,
|
'messages': msg_list,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
# """
|
|
||||||
# Handler helper functions pertaining to the article editor
|
|
||||||
# """
|
|
||||||
# import json
|
|
||||||
# import uuid
|
|
||||||
|
|
||||||
# from flask import (
|
|
||||||
# flash, redirect, url_for, render_template, Markup)
|
|
||||||
# from flask_login import current_user
|
|
||||||
|
|
||||||
# from amanuensis.lexicon import (
|
|
||||||
# get_player_characters,
|
|
||||||
# get_player_drafts,
|
|
||||||
# get_draft,
|
|
||||||
# title_constraint_analysis,
|
|
||||||
# content_constraint_analysis)
|
|
||||||
# from amanuensis.models import LexiconModel
|
|
||||||
# from amanuensis.parser import (
|
|
||||||
# normalize_title,
|
|
||||||
# parse_raw_markdown)
|
|
||||||
# from amanuensis.parser.core import RenderableVisitor
|
|
||||||
|
|
||||||
|
|
||||||
# class PreviewHtmlRenderer(RenderableVisitor):
|
|
||||||
# def __init__(self, lexicon):
|
|
||||||
# with lexicon.ctx.read('info') as info:
|
|
||||||
# self.article_map = {
|
|
||||||
# title: article.character
|
|
||||||
# for title, article in info.items()
|
|
||||||
# }
|
|
||||||
# self.citations = []
|
|
||||||
# self.contents = ""
|
|
||||||
|
|
||||||
# def TextSpan(self, span):
|
|
||||||
# return span.innertext
|
|
||||||
|
|
||||||
# def LineBreak(self, span):
|
|
||||||
# return '<br>'
|
|
||||||
|
|
||||||
# def ParsedArticle(self, span):
|
|
||||||
# self.contents = '\n'.join(span.recurse(self))
|
|
||||||
# return self
|
|
||||||
|
|
||||||
# def BodyParagraph(self, span):
|
|
||||||
# return f'<p>{"".join(span.recurse(self))}</p>'
|
|
||||||
|
|
||||||
# def SignatureParagraph(self, span):
|
|
||||||
# return (
|
|
||||||
# '<hr><span class="signature"><p>'
|
|
||||||
# f'{"".join(span.recurse(self))}'
|
|
||||||
# '</p></span>'
|
|
||||||
# )
|
|
||||||
|
|
||||||
# def BoldSpan(self, span):
|
|
||||||
# return f'<b>{"".join(span.recurse(self))}</b>'
|
|
||||||
|
|
||||||
# def ItalicSpan(self, span):
|
|
||||||
# return f'<i>{"".join(span.recurse(self))}</i>'
|
|
||||||
|
|
||||||
# def CitationSpan(self, span):
|
|
||||||
# if span.cite_target in self.article_map:
|
|
||||||
# if self.article_map.get(span.cite_target):
|
|
||||||
# link_class = '[extant]'
|
|
||||||
# else:
|
|
||||||
# link_class = '[phantom]'
|
|
||||||
# else:
|
|
||||||
# link_class = '[new]'
|
|
||||||
# self.citations.append(f'{span.cite_target} {link_class}')
|
|
||||||
# return f'<u>{"".join(span.recurse(self))}</u>[{len(self.citations)}]'
|
|
||||||
|
|
||||||
|
|
||||||
# def update_draft(lexicon: LexiconModel, article_json):
|
|
||||||
# """
|
|
||||||
# Update a draft and perform analysis on it
|
|
||||||
# """
|
|
||||||
# # Check if the update is permitted
|
|
||||||
# aid = article_json.get('aid')
|
|
||||||
# article = get_draft(lexicon, aid)
|
|
||||||
# if not article:
|
|
||||||
# raise ValueError("missing article")
|
|
||||||
# if lexicon.cfg.character.get(article.character).player != current_user.uid:
|
|
||||||
# return ValueError("bad user")
|
|
||||||
# if article.status.approved:
|
|
||||||
# raise ValueError("bad status")
|
|
||||||
|
|
||||||
# # Perform the update
|
|
||||||
# title = article_json.get('title')
|
|
||||||
# contents = article_json.get('contents')
|
|
||||||
# status = article_json.get('status')
|
|
||||||
|
|
||||||
# parsed = parse_raw_markdown(contents)
|
|
||||||
|
|
||||||
# # HTML parsing
|
|
||||||
# preview = parsed.render(PreviewHtmlRenderer(lexicon))
|
|
||||||
# # Constraint analysis
|
|
||||||
# title_infos, title_warnings, title_errors = title_constraint_analysis(
|
|
||||||
# lexicon, current_user, article.character, title)
|
|
||||||
# content_infos, content_warnings, content_errors = content_constraint_analysis(
|
|
||||||
# lexicon, current_user, article.character, parsed)
|
|
||||||
# if any(title_errors) or any(content_errors):
|
|
||||||
# status['ready'] = False
|
|
||||||
|
|
||||||
# # Article update
|
|
||||||
# filename = f'{article.character}.{aid}'
|
|
||||||
# with lexicon.ctx.draft.edit(filename) as draft:
|
|
||||||
# draft.title = normalize_title(title)
|
|
||||||
# draft.contents = contents
|
|
||||||
# draft.status.ready = status.get('ready', False)
|
|
||||||
|
|
||||||
# # Return canonical information to editor
|
|
||||||
# return {
|
|
||||||
# 'title': draft.title,
|
|
||||||
# 'status': {
|
|
||||||
# 'ready': draft.status.ready,
|
|
||||||
# 'approved': draft.status.approved,
|
|
||||||
# },
|
|
||||||
# 'rendered': preview.contents,
|
|
||||||
# 'citations': preview.citations,
|
|
||||||
# 'info': title_infos + content_infos,
|
|
||||||
# 'warning': title_warnings + content_warnings,
|
|
||||||
# 'error': title_errors + content_errors,
|
|
||||||
# }
|
|
Loading…
Reference in New Issue