Compare commits
2 Commits
6aadbaaf0c
...
837ce735aa
Author | SHA1 | Date |
---|---|---|
Tim Van Baak | 837ce735aa | |
Tim Van Baak | 241ec45430 |
|
@ -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, update
|
from sqlalchemy import select
|
||||||
|
|
||||||
from amanuensis.db import *
|
from amanuensis.db import *
|
||||||
from amanuensis.errors import ArgumentError, BackendArgumentTypeError
|
from amanuensis.errors import ArgumentError, BackendArgumentTypeError
|
||||||
|
@ -52,13 +52,3 @@ 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()
|
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
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,12 +2,11 @@
|
||||||
Module encapsulating all markdown parsing functionality.
|
Module encapsulating all markdown parsing functionality.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .core import RenderableVisitor, Renderable
|
from .core import RenderableVisitor
|
||||||
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: [],
|
||||||
messages: []
|
errors: []
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The nonce of the last-made update request. */
|
/** The nonce of the last-made update request. */
|
||||||
|
@ -33,11 +33,13 @@
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +51,7 @@
|
||||||
{
|
{
|
||||||
// Enable or disable controls
|
// Enable or disable controls
|
||||||
const isEditable = article.state == ArticleState.DRAFT;
|
const isEditable = article.state == ArticleState.DRAFT;
|
||||||
const blocked = preview.messages.filter(msg => msg.severity == 2).length > 0;
|
const blocked = preview.errors.filter(err => err.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";
|
||||||
|
@ -67,7 +69,7 @@
|
||||||
|
|
||||||
// Fill in the status message block
|
// Fill in the status message block
|
||||||
let statuses = "<ol>";
|
let statuses = "<ol>";
|
||||||
preview.messages.forEach(err => statuses += "<li>" + JSON.stringify(err) + "</li>");
|
preview.errors.forEach(err => statuses += "<li>" + JSON.stringify(err) + "</li>");
|
||||||
statuses += "<ol>";
|
statuses += "<ol>";
|
||||||
document.getElementById("preview-control").innerHTML = statuses;
|
document.getElementById("preview-control").innerHTML = statuses;
|
||||||
}
|
}
|
||||||
|
@ -80,7 +82,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.messages = data.messages;
|
preview.errors = data.errors;
|
||||||
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,6 +59,51 @@ 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
|
||||||
|
@ -84,65 +129,39 @@ 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,
|
||||||
'messages': msg_list,
|
'errors': errors,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@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']
|
||||||
# Extract the submitted content
|
article.body = request.json['body']
|
||||||
new_title = request.json['title']
|
article.state = ArticleState(request.json['state'])
|
||||||
new_body = request.json['body']
|
g.db.session.commit()
|
||||||
new_state = ArticleState(request.json['state'])
|
parsed = parse_raw_markdown(article.body)
|
||||||
|
|
||||||
# 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': updated_article.title,
|
'title': article.title,
|
||||||
'rendered': preview_result.contents,
|
'rendered': preview_result.contents,
|
||||||
'state': updated_article.state.value,
|
'state': article.state.value,
|
||||||
'ersatz': updated_article.ersatz,
|
'ersatz': article.ersatz,
|
||||||
'citations': preview_result.citations,
|
'citations': preview_result.citations,
|
||||||
'messages': msg_list,
|
'errors': errors,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
# """
|
||||||
|
# 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