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 uuid import UUID
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy import select
|
||||
|
||||
from amanuensis.db import *
|
||||
from amanuensis.errors import ArgumentError, BackendArgumentTypeError
|
||||
|
@ -52,13 +52,3 @@ def try_from_public_id(db: DbContext, public_id: UUID) -> Optional[Article]:
|
|||
return db(
|
||||
select(Article).where(Article.public_id == public_id)
|
||||
).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.
|
||||
"""
|
||||
|
||||
from .core import RenderableVisitor, Renderable
|
||||
from .core import RenderableVisitor
|
||||
from .helpers import normalize_title, filesafe_title, titlesort
|
||||
from .parsing import parse_raw_markdown
|
||||
|
||||
__all__ = [
|
||||
"Renderable",
|
||||
"RenderableVisitor",
|
||||
"normalize_title",
|
||||
"filesafe_title",
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
title: undefined,
|
||||
rendered: undefined,
|
||||
citations: [],
|
||||
messages: []
|
||||
errors: []
|
||||
}
|
||||
|
||||
/** The nonce of the last-made update request. */
|
||||
|
@ -33,11 +33,13 @@
|
|||
// Stake a claim on the nonce, potentially overwriting a previous
|
||||
// nonce value.
|
||||
const nonce_local = 1 + Math.random();
|
||||
console.log("ifNoFurtherChanges: timeout=" + timeout.toString() + ", nonce=" + nonce_local.toString());
|
||||
nonce = nonce_local;
|
||||
// Wait to see if this call is overwritten in turn.
|
||||
setTimeout(() => {
|
||||
if (nonce == nonce_local)
|
||||
{
|
||||
console.log("Executing for: " + nonce.toString());
|
||||
callback();
|
||||
nonce = 0;
|
||||
}
|
||||
|
@ -49,7 +51,7 @@
|
|||
{
|
||||
// Enable or disable controls
|
||||
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-content").disabled = !isEditable;
|
||||
document.getElementById("button-submit").innerText = isEditable ? "Submit article" : "Edit article";
|
||||
|
@ -67,7 +69,7 @@
|
|||
|
||||
// Fill in the status message block
|
||||
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>";
|
||||
document.getElementById("preview-control").innerHTML = statuses;
|
||||
}
|
||||
|
@ -80,7 +82,7 @@
|
|||
preview.title = data.title;
|
||||
preview.rendered = data.rendered;
|
||||
preview.citations = data.citations;
|
||||
preview.messages = data.messages;
|
||||
preview.errors = data.errors;
|
||||
refreshEditor();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ 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.parser import *
|
||||
from amanuensis.parser.core import Renderable
|
||||
from amanuensis.server.helpers import lexicon_param, player_required
|
||||
|
||||
|
||||
|
@ -59,6 +59,51 @@ class PreviewHtmlRenderer(RenderableVisitor):
|
|||
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("/")
|
||||
@lexicon_param
|
||||
@player_required
|
||||
|
@ -84,65 +129,39 @@ def open(lexicon_name, article_id):
|
|||
@lexicon_param
|
||||
@player_required
|
||||
def load(lexicon_name, article_id):
|
||||
# Get the article
|
||||
article = artiq.try_from_public_id(g.db, article_id)
|
||||
if not article:
|
||||
return abort(404)
|
||||
|
||||
# Generate the preview HTML
|
||||
parsed = parse_raw_markdown(article.body)
|
||||
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
|
||||
|
||||
# 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])
|
||||
errors = constraint_check(parsed)
|
||||
return {
|
||||
'title': article.title,
|
||||
'rendered': preview_result.contents,
|
||||
'state': article.state.value,
|
||||
'ersatz': article.ersatz,
|
||||
'citations': preview_result.citations,
|
||||
'messages': msg_list,
|
||||
'errors': errors,
|
||||
}
|
||||
|
||||
|
||||
@bp.post("/<uuid:article_id>/update")
|
||||
def update(lexicon_name, article_id):
|
||||
# Get the article
|
||||
article = artiq.try_from_public_id(g.db, article_id)
|
||||
if not article:
|
||||
return abort(404)
|
||||
|
||||
# Extract the submitted content
|
||||
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)
|
||||
article.title = request.json['title']
|
||||
article.body = request.json['body']
|
||||
article.state = ArticleState(request.json['state'])
|
||||
g.db.session.commit()
|
||||
parsed = parse_raw_markdown(article.body)
|
||||
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
|
||||
|
||||
# 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])
|
||||
errors = constraint_check(parsed)
|
||||
return {
|
||||
'title': updated_article.title,
|
||||
'title': article.title,
|
||||
'rendered': preview_result.contents,
|
||||
'state': updated_article.state.value,
|
||||
'ersatz': updated_article.ersatz,
|
||||
'state': article.state.value,
|
||||
'ersatz': article.ersatz,
|
||||
'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