add motd schema and motd editor
This commit is contained in:
@@ -218,10 +218,14 @@ def should_collapse(text, surrounding):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def babycode_to_html(s):
|
def babycode_to_html(s, banned_tags=None):
|
||||||
|
allowed_tags = list(TAGS.keys())
|
||||||
|
if banned_tags is not None:
|
||||||
|
for tag in banned_tags:
|
||||||
|
allowed_tags.remove(tag)
|
||||||
subj = escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
subj = escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
||||||
parser = Parser(subj)
|
parser = Parser(subj)
|
||||||
parser.valid_bbcode_tags = TAGS.keys()
|
parser.valid_bbcode_tags = allowed_tags
|
||||||
parser.bbcode_tags_only_text_children = TEXT_ONLY
|
parser.bbcode_tags_only_text_children = TEXT_ONLY
|
||||||
parser.valid_emotes = EMOJI.keys()
|
parser.valid_emotes = EMOJI.keys()
|
||||||
|
|
||||||
|
|||||||
@@ -394,3 +394,19 @@ class BookmarkedThreads(Model):
|
|||||||
|
|
||||||
def get_thread(self):
|
def get_thread(self):
|
||||||
return Threads.find({'id': self.thread_id})
|
return Threads.find({'id': self.thread_id})
|
||||||
|
|
||||||
|
|
||||||
|
class MOTD(Model):
|
||||||
|
table = 'motd'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def has_motd(cls):
|
||||||
|
q = 'SELECT EXISTS(SELECT 1 FROM motd) as e'
|
||||||
|
res = db.fetch_one(q)['e']
|
||||||
|
return int(res) == 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all(cls):
|
||||||
|
q = 'SELECT id FROM motd'
|
||||||
|
res = db.query(q)
|
||||||
|
return [MOTD.find({'id': i['id']}) for i in res]
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ def babycode_preview():
|
|||||||
markup = request.json.get('markup')
|
markup = request.json.get('markup')
|
||||||
if not markup or not isinstance(markup, str):
|
if not markup or not isinstance(markup, str):
|
||||||
return {'error': 'markup field missing or invalid type'}, 400
|
return {'error': 'markup field missing or invalid type'}, 400
|
||||||
rendered = babycode_to_html(markup)
|
banned_tags = request.json.get('banned_tags', [])
|
||||||
|
rendered = babycode_to_html(markup, banned_tags)
|
||||||
return {'html': rendered}
|
return {'html': rendered}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, request, redirect, url_for
|
Blueprint, render_template, request, redirect, url_for,
|
||||||
|
flash
|
||||||
)
|
)
|
||||||
from .users import get_active_user, is_logged_in
|
from .users import get_active_user, is_logged_in
|
||||||
from ..models import Users, PasswordResetLinks
|
from ..models import Users, PasswordResetLinks, MOTD
|
||||||
|
from ..constants import InfoboxKind
|
||||||
|
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||||
from ..db import db
|
from ..db import db
|
||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
@@ -55,3 +58,47 @@ def create_reset_pass(user_id):
|
|||||||
@bp.get('/panel')
|
@bp.get('/panel')
|
||||||
def panel():
|
def panel():
|
||||||
return render_template('mod/panel.html')
|
return render_template('mod/panel.html')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get('/motd')
|
||||||
|
def motd_editor():
|
||||||
|
current = MOTD.get_all()[0] if MOTD.has_motd() else None
|
||||||
|
return render_template('mod/motd.html', current=current)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/motd')
|
||||||
|
def motd_editor_form():
|
||||||
|
orig_body = request.form.get('body', default='')
|
||||||
|
title = request.form.get('title', default='')
|
||||||
|
data = {
|
||||||
|
'title': title,
|
||||||
|
'body_original_markup': orig_body,
|
||||||
|
'body_rendered': babycode_to_html(orig_body, banned_tags=['img', 'spoiler']),
|
||||||
|
'format_version': BABYCODE_VERSION,
|
||||||
|
'edited_at': int(time.time()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if MOTD.has_motd():
|
||||||
|
motd = MOTD.get_all()[0]
|
||||||
|
motd.update(data)
|
||||||
|
message = 'MOTD updated.'
|
||||||
|
else:
|
||||||
|
data['created_at'] = int(time.time())
|
||||||
|
data['user_id'] = get_active_user().id
|
||||||
|
motd = MOTD.create(data)
|
||||||
|
message = 'MOTD created.'
|
||||||
|
|
||||||
|
flash(message, InfoboxKind.INFO)
|
||||||
|
return redirect(url_for('.motd_editor'))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/motd/delete')
|
||||||
|
def motd_delete():
|
||||||
|
if not MOTD.has_motd():
|
||||||
|
flash('No MOTD to delete.', InfoboxKind.WARN)
|
||||||
|
return redirect(url_for('.motd_editor'))
|
||||||
|
|
||||||
|
current = MOTD.get_all()[0]
|
||||||
|
current.delete()
|
||||||
|
flash('MOTD deleted.', InfoboxKind.INFO)
|
||||||
|
return redirect(url_for('.motd_editor'))
|
||||||
|
|||||||
@@ -120,6 +120,18 @@ SCHEMA = [
|
|||||||
UNIQUE(collection_id, thread_id)
|
UNIQUE(collection_id, thread_id)
|
||||||
)""",
|
)""",
|
||||||
|
|
||||||
|
"""CREATE TABLE IF NOT EXISTS "motd" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"body_original_markup" TEXT NOT NULL,
|
||||||
|
"body_rendered" TEXT NOT NULL,
|
||||||
|
"markup_language" TEXT NOT NULL DEFAULT 'babycode',
|
||||||
|
"format_version" INTEGER DEFAULT NULL,
|
||||||
|
"created_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
|
||||||
|
"edited_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
|
||||||
|
"user_id" REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)""",
|
||||||
|
|
||||||
# INDEXES
|
# INDEXES
|
||||||
"CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)",
|
"CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)",
|
"CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)",
|
||||||
|
|||||||
@@ -47,3 +47,9 @@
|
|||||||
<path d="M13 20H6C4.89543 20 4 19.1046 4 18V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V13M13 20L20 13M13 20V14C13 13.4477 13.4477 13 14 13H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M13 20H6C4.89543 20 4 19.1046 4 18V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V13M13 20L20 13M13 20V14C13 13.4477 13.4477 13 14 13H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro icn_megaphone(width=60) -%}
|
||||||
|
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 18V14M6 14H8L13 17V7L8 10H5C3.89543 10 3 10.8954 3 12V12C3 13.1046 3.89543 14 5 14H6ZM17 7L19 5M17 17L19 19M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
{%- endmacro %}
|
||||||
|
|||||||
@@ -39,18 +39,24 @@
|
|||||||
{% macro infobox(message, kind=InfoboxKind.INFO) %}
|
{% macro infobox(message, kind=InfoboxKind.INFO) %}
|
||||||
<div class="{{ "infobox " + InfoboxHTMLClass[kind] }}">
|
<div class="{{ "infobox " + InfoboxHTMLClass[kind] }}">
|
||||||
<span>
|
<span>
|
||||||
<div class="infobox-icon-container">
|
<div class="infobox-icon-container">
|
||||||
{%- if kind == InfoboxKind.INFO -%}
|
{%- if kind == InfoboxKind.INFO -%}
|
||||||
{{- icn_info() -}}
|
{{- icn_info() -}}
|
||||||
{%- elif kind == InfoboxKind.LOCK -%}
|
{%- elif kind == InfoboxKind.LOCK -%}
|
||||||
{{- icn_lock() -}}
|
{{- icn_lock() -}}
|
||||||
{%- elif kind == InfoboxKind.WARN -%}
|
{%- elif kind == InfoboxKind.WARN -%}
|
||||||
{{- icn_warn() -}}
|
{{- icn_warn() -}}
|
||||||
{%- elif kind == InfoboxKind.ERROR -%}
|
{%- elif kind == InfoboxKind.ERROR -%}
|
||||||
{{- icn_error() -}}
|
{{- icn_error() -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
{{ message }}
|
<span>
|
||||||
|
{% set m = message.split(';', maxsplit=1) %}
|
||||||
|
<strong>{{ m[0] }}</strong>
|
||||||
|
{%- if m[1] %}
|
||||||
|
{{ m[1] -}}
|
||||||
|
{%- endif -%}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
@@ -59,27 +65,38 @@
|
|||||||
<span class="timestamp" data-utc="{{ unix_ts }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></span>
|
<span class="timestamp" data-utc="{{ unix_ts }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></span>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro babycode_editor_component(ta_name, ta_placeholder="Post body", optional=False, prefill="") %}
|
{% macro babycode_editor_component(ta_name, ta_placeholder="Post body", optional=False, prefill="", banned_tags=[]) %}
|
||||||
<div class="babycode-editor-container">
|
<div class="babycode-editor-container">
|
||||||
|
<input type="hidden" id="babycode-banned-tags" value="{{banned_tags | unique | list | tojson | forceescape}}">
|
||||||
<div class="tab-buttons">
|
<div class="tab-buttons">
|
||||||
<button type=button class="tab-button active" data-target-id="tab-edit">Write</button>
|
<button type=button class="tab-button active" data-target-id="tab-edit">Write</button>
|
||||||
<button type=button class="tab-button" data-target-id="tab-preview">Preview</button>
|
<button type=button class="tab-button" data-target-id="tab-preview">Preview</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content active" id="tab-edit">
|
<div class="tab-content active" id="tab-edit">
|
||||||
<span class="babycode-button-container">
|
<span class="babycode-button-container">
|
||||||
<button class="babycode-button" type=button id="post-editor-bold" title="Insert Bold"><strong>B</strong></button>
|
<button class="babycode-button" type=button id="post-editor-bold" title="Insert Bold" {{"disabled" if "b" in banned_tags else ""}}><strong>B</strong></button>
|
||||||
<button class="babycode-button" type=button id="post-editor-italics" title="Insert Italics"><em>I</em></button>
|
<button class="babycode-button" type=button id="post-editor-italics" title="Insert Italics" {{"disabled" if "i" in banned_tags else ""}}><em>I</em></button>
|
||||||
<button class="babycode-button" type=button id="post-editor-strike" title="Insert Strikethrough"><del>S</del></button>
|
<button class="babycode-button" type=button id="post-editor-strike" title="Insert Strikethrough" {{"disabled" if "s" in banned_tags else ""}}><del>S</del></button>
|
||||||
<button class="babycode-button" type=button id="post-editor-underline" title="Insert Underline"><u>U</u></button>
|
<button class="babycode-button" type=button id="post-editor-underline" title="Insert Underline" {{"disabled" if "u" in banned_tags else ""}}><u>U</u></button>
|
||||||
<button class="babycode-button" type=button id="post-editor-url" title="Insert Link"><code>://</code></button>
|
<button class="babycode-button" type=button id="post-editor-url" title="Insert Link" {{"disabled" if "url" in banned_tags else ""}}><code>://</code></button>
|
||||||
<button class="babycode-button" type=button id="post-editor-code" title="Insert Code block"><code></></code></button>
|
<button class="babycode-button" type=button id="post-editor-code" title="Insert Code block" {{"disabled" if "code" in banned_tags else ""}}><code></></code></button>
|
||||||
<button class="babycode-button contain-svg" type=button id="post-editor-img" title="Insert Image">{{ icn_image() }}</button>
|
<button class="babycode-button contain-svg" type=button id="post-editor-img" title="Insert Image" {{"disabled" if "img" in banned_tags else ""}}>{{ icn_image() }}</button>
|
||||||
<button class="babycode-button" type=button id="post-editor-ol" title="Insert Ordered list">1.</button>
|
<button class="babycode-button" type=button id="post-editor-ol" title="Insert Ordered list" {{"disabled" if "ol" in banned_tags else ""}}>1.</button>
|
||||||
<button class="babycode-button" type=button id="post-editor-ul" title="Insert Unordered list">•</button>
|
<button class="babycode-button" type=button id="post-editor-ul" title="Insert Unordered list" {{"disabled" if "u;" in banned_tags else ""}}>•</button>
|
||||||
<button class="babycode-button contain-svg" type=button id="post-editor-spoiler" title="Insert spoiler">{{ icn_spoiler() }}</button>
|
<button class="babycode-button contain-svg" type=button id="post-editor-spoiler" title="Insert spoiler" {{"disabled" if "spoiler" in banned_tags else ""}}>{{ icn_spoiler() }}</button>
|
||||||
</span>
|
</span>
|
||||||
<textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }}>{{ prefill }}</textarea>
|
<textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }} autocomplete="off">{{ prefill }}</textarea>
|
||||||
<a href="{{ url_for("app.babycode_guide") }}" target="_blank">babycode guide</a>
|
<a href="{{ url_for("app.babycode_guide") }}" target="_blank">babycode guide</a>
|
||||||
|
{% if banned_tags %}
|
||||||
|
<div>Forbidden tags:</div>
|
||||||
|
<div>
|
||||||
|
<ul class="horizontal">
|
||||||
|
{% for tag in banned_tags | unique %}
|
||||||
|
<li><code class="inline-code">[{{ tag }}]</code></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-content" id="tab-preview">
|
<div class="tab-content" id="tab-preview">
|
||||||
<div id="babycode-preview-errors-container">Type something!</div>
|
<div id="babycode-preview-errors-container">Type something!</div>
|
||||||
|
|||||||
17
app/templates/mod/motd.html
Normal file
17
app/templates/mod/motd.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{% from 'common/macros.html' import babycode_editor_component %}
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}editing MOTD{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="darkbg settings-container">
|
||||||
|
<h1>Edit Message of the Day</h1>
|
||||||
|
<p>The Message of the Day will show up on the main page and in every topic.</p>
|
||||||
|
<form method="POST">
|
||||||
|
<label for="title">Title</label>
|
||||||
|
<input name="title" id="title" type="text" required autocomplete="off" placeholder="Required" value="{{ current.title }}"><br>
|
||||||
|
<label for="body">Body</label>
|
||||||
|
{{ babycode_editor_component('body', ta_placeholder='MOTD body (required)', banned_tags=['img', 'spoiler'], prefill=current.body_original_markup) }}
|
||||||
|
<input type="submit" value="Save">
|
||||||
|
<input class="critical" type="submit" formaction="{{ url_for('mod.motd_delete') }}" value="Delete MOTD" formnovalidate {{"disabled" if not current else ""}}>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ url_for('mod.user_list') }}">User list</a></li>
|
<li><a href="{{ url_for('mod.user_list') }}">User list</a></li>
|
||||||
<li><a href="{{ url_for('mod.sort_topics') }}">Sort topics</a></li>
|
<li><a href="{{ url_for('mod.sort_topics') }}">Sort topics</a></li>
|
||||||
|
<li><a href="{{ url_for('mod.motd_editor') }}">Message of the Day</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{% if topic['is_locked'] %}
|
{% if topic['is_locked'] %}
|
||||||
{{ infobox("This topic is locked. Only moderators can create new threads.", InfoboxKind.INFO) }}
|
{{ infobox("This topic is locked.;Only moderators can create new threads.", InfoboxKind.INFO) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if threads_list | length == 0 %}
|
{% if threads_list | length == 0 %}
|
||||||
|
|||||||
@@ -147,13 +147,17 @@
|
|||||||
if (markup === "" || markup === previousMarkup) {
|
if (markup === "" || markup === previousMarkup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const bannedTags = JSON.parse(document.getElementById('babycode-banned-tags').value);
|
||||||
previousMarkup = markup;
|
previousMarkup = markup;
|
||||||
const req = await fetch(previewEndpoint, {
|
const req = await fetch(previewEndpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({markup: markup})
|
body: JSON.stringify({
|
||||||
|
markup: markup,
|
||||||
|
banned_tags: bannedTags,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
if (!req.ok) {
|
if (!req.ok) {
|
||||||
switch (req.status) {
|
switch (req.status) {
|
||||||
|
|||||||
@@ -1082,7 +1082,6 @@ ul.horizontal, ol.horizontal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.new-concept-notification.hidden {
|
.new-concept-notification.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user