From 9c4f2712597c9c402e90ffc9fe1f3ed39ca5411d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Thu, 16 Apr 2026 23:11:19 +0300 Subject: [PATCH] add most mod routes --- app/__init__.py | 3 +- app/auth.py | 12 ++++- app/lib/babycode.py | 2 +- app/models.py | 21 ++++++++- app/routes/mod.py | 66 ++++++++++++++++++++++++--- app/routes/threads.py | 8 +++- app/templates/base.html | 8 ++-- app/templates/common/macros.html | 6 ++- app/templates/mod/edit_topic.html | 13 ++++++ app/templates/mod/new_topic.html | 13 ++++++ app/templates/threads/new_thread.html | 19 ++++++++ app/templates/threads/thread.html | 20 ++++++-- app/templates/topics/topic.html | 24 +++++++++- data/static/css/style.css | 35 ++++++++++---- 14 files changed, 216 insertions(+), 34 deletions(-) create mode 100644 app/templates/mod/edit_topic.html create mode 100644 app/templates/mod/new_topic.html create mode 100644 app/templates/threads/new_thread.html diff --git a/app/__init__.py b/app/__init__.py index b7dd44e..f9f0f91 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -231,7 +231,8 @@ def create_app(): @app.before_request def make_session_permanent(): - session.permanent = True + if is_logged_in(): + session.permanent = True commit = '' with open('.git/refs/heads/main') as f: diff --git a/app/auth.py b/app/auth.py index e8fac86..30cf518 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,4 +1,4 @@ -from flask import session, flash, redirect, url_for +from flask import session, flash, redirect, url_for, abort from .models import Sessions, Users from argon2 import PasswordHasher from functools import wraps @@ -51,3 +51,13 @@ def login_required(view_func): return redirect(url_for('users.log_in_page')) return view_func(*args, **kwargs) return wrapper + +def mod_only(view_func): + @wraps(view_func) + def wrapper(*args, **kwargs): + if not is_logged_in(): + abort(403) + if not get_active_user().is_mod(): + abort(403) + return view_func(*args, **kwargs) + return wrapper diff --git a/app/lib/babycode.py b/app/lib/babycode.py index 040f948..2f208c0 100644 --- a/app/lib/babycode.py +++ b/app/lib/babycode.py @@ -400,7 +400,7 @@ def tag_image(children, attr): def tag_quote(children, attr): if attr: - quotee = attr.strip() + quotee = f'Quoting: {attr.strip()}' else: quotee = 'Quote' diff --git a/app/models.py b/app/models.py index b8ebbee..116f96c 100644 --- a/app/models.py +++ b/app/models.py @@ -123,6 +123,22 @@ class Topics(Model): GROUP BY topics.id ORDER BY topics.sort_order ASC""" return db.query(q) + @classmethod + def new(_cls, name: str, description: str) -> Topics: + from slugify import slugify + name = name.strip() + description = description.strip() + now = int(time.time()) + slug = f'{slugify(name)}-{now}' + + topic_count = Topics.count() + return Topics.create({ + 'name': name, + 'description': description, + 'slug': slug, + 'sort_order': topic_count + 1, + }) + def get_threads(self, per_page, page, sort_by = 'activity'): order_clause = '' if sort_by == 'thread': @@ -198,10 +214,13 @@ class Topics(Model): users u ON u.id = posts.user_id WHERE threads.topic_id = ? - ORDER BY threads.created_at DESC""" + ORDER BY threads.created_at DESC""" return db.query(q, self.id) + def locked(self): + return bool(self.is_locked) + class Threads(Model): table = 'threads' diff --git a/app/routes/mod.py b/app/routes/mod.py index 90747cb..7e25da0 100644 --- a/app/routes/mod.py +++ b/app/routes/mod.py @@ -1,27 +1,81 @@ -from flask import Blueprint - +from flask import Blueprint, abort, redirect, url_for, request, render_template +from ..auth import is_logged_in, get_active_user +from ..models import Topics, Threads bp = Blueprint('mod', __name__, url_prefix='/mod/') +@bp.before_request +def mod_only(): + if not is_logged_in(): + abort(403) + if not get_active_user().is_mod(): + abort(403) + @bp.get('/') def index(): return 'stub' @bp.get('/topics/new') def new_topic(): - return 'stub' + return render_template('mod/new_topic.html') + +@bp.post('/topics/new') +def new_topic_post(): + topic = Topics.new(request.form.get('name'), request.form.get('description')) + return redirect(url_for('topics.topic', slug=topic.slug)) @bp.get('/topics/sort') def sort_topics(): return 'stub' +@bp.get('/topics//edit') +def edit_topic(topic_id): + topic = Topics.find({'id': topic_id}) + if not topic: + abort(404) + return render_template('mod/edit_topic.html', topic=topic) + +@bp.post('/topics//edit') +def edit_topic_post(topic_id): + topic = Topics.find({'id': topic_id}) + if not topic: + abort(404) + topic.update({ + 'name': request.form.get('name').strip(), + 'description': request.form.get('description').strip(), + }) + return redirect(url_for('topics.topic', slug=topic.slug)) + +@bp.post('/topics//lock') +def lock_topic(topic_id): + topic = Topics.find({'id': topic_id}) + if not topic: + abort(404) + topic.update({'is_locked': request.form.get('lock', default=0)}) + return redirect(url_for('topics.topic', slug=topic.slug)) + @bp.post('/threads//move') def move_thread(thread_id): - return 'stub' + thread = Threads.find({'id': thread_id}) + if not thread: + abort(404) + target_topic = Topics.find({'id': request.form.get('new_topic_id', default=None)}) + if not target_topic: + abort(404) + thread.update({'topic_id': target_topic.id}) + return redirect(url_for('threads.thread', slug=thread.slug)) @bp.post('/threads//lock') def lock_thread(thread_id): - return 'stub' + thread = Threads.find({'id': thread_id}) + if not thread: + abort(404) + thread.update({'is_locked': request.form.get('lock')}) + return redirect(url_for('threads.thread', slug=thread.slug)) @bp.post('/threads//sticky') def sticky_thread(thread_id): - return 'stub' + thread = Threads.find({'id': thread_id}) + if not thread: + abort(404) + thread.update({'is_stickied': request.form.get('sticky')}) + return redirect(url_for('threads.thread', slug=thread.slug)) diff --git a/app/routes/threads.py b/app/routes/threads.py index 066bbec..1b7ae7f 100644 --- a/app/routes/threads.py +++ b/app/routes/threads.py @@ -52,5 +52,11 @@ def feed(slug): return 'stub' @bp.get('/new') +@login_required def new(): - return 'stub' + topics = Topics.select() + try: + selected_topic = int(request.args.get('topic_id', default=None)) + except ValueError, TypeError: + selected_topic = None + return render_template('threads/new_thread.html', topics=topics, selected_topic=selected_topic) diff --git a/app/templates/base.html b/app/templates/base.html index ccd5e5c..0ab750b 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -12,10 +12,8 @@ {%- endif -%} -
- {%- include 'common/topnav.html' -%} - {%- block content -%}{%- endblock -%} - {%- include 'common/footer.html' -%} -
+ {%- include 'common/topnav.html' -%} + {%- block content -%}{%- endblock -%} + {%- include 'common/footer.html' -%} diff --git a/app/templates/common/macros.html b/app/templates/common/macros.html index cdc6d49..28f74cf 100644 --- a/app/templates/common/macros.html +++ b/app/templates/common/macros.html @@ -83,7 +83,9 @@ {% macro babycode_editor_component( placeholder='Post content', - prefill='' + prefill='', + required=true, + id='babycode-content' ) -%} {%- call(idx) tabs(prefix='babycode', labels=['Write', 'Preview']) -%} {%- if idx == 0 -%} @@ -101,7 +103,7 @@ babycode help - + {%- endif -%} {%- endcall -%} {%- endmacro %} diff --git a/app/templates/mod/edit_topic.html b/app/templates/mod/edit_topic.html new file mode 100644 index 0000000..64a939a --- /dev/null +++ b/app/templates/mod/edit_topic.html @@ -0,0 +1,13 @@ +{%- from 'common/macros.html' import subheader -%} +{%- extends 'base.html' -%} +{%- block title -%}editing topic {{topic.name}}{%- endblock -%} +{%- block content -%} +{{subheader('Editing topic %s' % topic.name, 'To preserve history, the URL of the topic can not be changed.')}} +
+ + + + + +
+{%- endblock -%} diff --git a/app/templates/mod/new_topic.html b/app/templates/mod/new_topic.html new file mode 100644 index 0000000..34df62f --- /dev/null +++ b/app/templates/mod/new_topic.html @@ -0,0 +1,13 @@ +{%- from 'common/macros.html' import subheader -%} +{%- extends 'base.html' -%} +{%- block title -%}creating a topic{%- endblock -%} +{%- block content -%} +{{subheader('Create topic', 'The new topic will appear at the bottom of the current topic list. You can sort it later.')}} +
+ + + + + +
+{%- endblock -%} diff --git a/app/templates/threads/new_thread.html b/app/templates/threads/new_thread.html new file mode 100644 index 0000000..b61945f --- /dev/null +++ b/app/templates/threads/new_thread.html @@ -0,0 +1,19 @@ +{%- from 'common/macros.html' import subheader, babycode_editor_component -%} +{%- extends 'base.html' -%} +{%- block title -%}drafting a thread{%- endblock -%} +{%- block content -%} +{{subheader('New thread')}} +
+ + + + + + {{ babycode_editor_component() }} + +
+{%- endblock -%} diff --git a/app/templates/threads/thread.html b/app/templates/threads/thread.html index 0915c92..c252076 100644 --- a/app/templates/threads/thread.html +++ b/app/templates/threads/thread.html @@ -1,10 +1,20 @@ {%- from 'common/macros.html' import subheader, timestamp, pager, babycode_editor_component -%} {%- from 'common/macros.html' import full_post with context -%} {%- extends 'base.html' -%} -{%- block title -%}{%- endblock -%} +{%- block title -%}{{thread.title}}{%- endblock -%} {%- block content -%} {%- set td -%} -Started by {{started_by.get_readable_name()}} in topic {{topic.name}} +
    +
  • Started by {{started_by.get_readable_name()}} in topic {{topic.name}}
  • +{%- if thread.locked() or thread.stickied() -%} + {%- if thread.locked() -%} +
  • Locked
  • + {%- endif -%} + {%- if thread.stickied() -%} +
  • Stickied
  • + {%- endif -%} +{%- endif -%} +
{%- endset -%} {%- call() subheader(thread.title, td) -%}
@@ -19,13 +29,13 @@ Started by Moderation actions
- - + +
- {%- for t in topics -%} {%- endfor -%} diff --git a/app/templates/topics/topic.html b/app/templates/topics/topic.html index 00e4413..973471d 100644 --- a/app/templates/topics/topic.html +++ b/app/templates/topics/topic.html @@ -2,10 +2,18 @@ {%- extends 'base.html' -%} {%- block title -%}browsing topic {{topic.name}}{%- endblock -%} {%- block content -%} -{%- call() subheader(('Threads in "%s"' % topic.name), topic.description) -%} +{%- set td -%} +
    +
  • {{topic.description}}
  • + {%- if topic.locked() -%} +
  • Locked
  • + {%- endif -%} +
+{%- endset -%} +{%- call() subheader(('Threads in "%s"' % topic.name), td) -%}
Actions - {%- if is_logged_in() -%} + {%- if is_logged_in() and get_active_user().can_post_to_topic(topic) -%} New thread {%- endif -%} Subscribe via RSS @@ -20,13 +28,23 @@ {%- if is_mod() -%}
Moderation actions + Edit + + + +
{%- endif -%} +{%- if threads | length > 0 -%}
Page {{- pager(page, page_count, args=request.args) -}}
+{%- endif -%} {%- endcall -%} +{%- if threads | length == 0 -%} +

There are no threads in this topic yet.{%- if is_logged_in() and get_active_user().can_post_to_topic(topic) %} Be the first to start a discussion!{%- endif -%}

+{%- endif -%} {%- for thread in threads -%}
{%- endfor -%} +{%- if threads | length > 0 -%}
Page {{- pager(page, page_count, args=request.args) -}}
+{%- endif -%} {%- endblock -%} diff --git a/data/static/css/style.css b/data/static/css/style.css index 75fd00f..4fcbf21 100644 --- a/data/static/css/style.css +++ b/data/static/css/style.css @@ -82,9 +82,6 @@ body { background-color: var(--bg-color-tertiary); font-family: Cadman; color: var(--font-color-main); -} - -#wrapper { margin: var(--big-padding) var(--wrapper-side-margin); } @@ -164,6 +161,10 @@ button, .linkbutton, input[type="submit"] { } } +.tab-container { + width: 100%; +} + .tab-bar { display: flex; gap: var(--base-padding); @@ -187,7 +188,6 @@ button, .linkbutton, input[type="submit"] { .babycode-editor { width: 100%; height: 150px; - resize: vertical; } .post-edit-form { @@ -202,6 +202,7 @@ input[type="text"], input[type="password"], textarea, select { background-color: var(--main-color); border-radius: var(--border-radius); border: solid var(--border-thickness) var(--border-color); + resize: vertical; padding: var(--small-padding) var(--medium-padding); margin: var(--base-padding) 0px; @@ -211,6 +212,10 @@ input[type="text"], input[type="password"], textarea, select { } } +textarea { + font-family: 'Atkinson Hyperlegible Mono' +} + h1 { margin: 0; } @@ -324,10 +329,14 @@ ul.horizontal, ol.horizontal { padding: 0; gap: var(--base-padding); - & li { + & li:not(.visible) { list-style-type: none; } + & li.visible { + margin-left: var(--big-padding); + } + &.wrap { flex-wrap: wrap; } @@ -456,7 +465,6 @@ footer { flex-wrap: wrap; gap: var(--base-padding); width: fit-content; - justify-content: center; } .actions-group { @@ -584,6 +592,15 @@ footer { flex-wrap: wrap; } +form.full-width { + display: flex; + flex-direction: column; + align-items: start; + &> textarea, &> select, &> input[type="text"], &> input[type="password"] { + width: 100%; + } +} + /* babycode tags */ .inline-code { background-color: var(--code-bg-color); @@ -688,9 +705,9 @@ a.mention { } @media (max-width: 768px) { - #wrapper { - margin-left: 0; - margin-right: 0; + body { + margin-left: 0; + margin-right: 0; } .settings-grid {