From 4aa4e58c58903fd52c1246a02ba5303d529ac2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Mon, 13 Apr 2026 20:03:26 +0300 Subject: [PATCH] start stubbing out endpoints --- app/__init__.py | 17 ++++++++- app/auth.py | 21 +++++++++-- app/models.py | 62 +++++++++++++++++-------------- app/routes/guides.py | 11 ++++++ app/routes/mod.py | 15 ++++++++ app/routes/threads.py | 11 ++++++ app/routes/topics.py | 20 +++++++--- app/routes/users.py | 38 +++++++++++++++++++ app/templates/common/footer.html | 6 +-- app/templates/common/macros.html | 64 ++++++++++++++++++++++++++++++++ app/templates/common/topnav.html | 22 ++++++++--- app/templates/topics/topic.html | 38 +++++++++++++++++++ app/templates/topics/topics.html | 11 +++++- app/util.py | 16 ++++++++ 14 files changed, 304 insertions(+), 48 deletions(-) create mode 100644 app/routes/guides.py create mode 100644 app/routes/mod.py create mode 100644 app/routes/threads.py create mode 100644 app/routes/users.py create mode 100644 app/templates/topics/topic.html create mode 100644 app/util.py diff --git a/app/__init__.py b/app/__init__.py index 7483263..244f06a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,7 +1,7 @@ from flask import Flask, session, request, render_template from dotenv import load_dotenv from .models import Avatars, Users, PostHistory, Posts, MOTD, BadgeUploads, Sessions -from .auth import digest, is_logged_in +from .auth import digest, is_logged_in, get_active_user from .constants import ( PermissionLevel, permission_level_string, InfoboxKind, InfoboxHTMLClass, @@ -10,6 +10,7 @@ from .constants import ( ) from .lib.babycode import babycode_to_html, babycode_to_rssxml, EMOJI, BABYCODE_VERSION from .lib.exceptions import SiteNameMissingException +from .util import get_post_url, dict_to_query_string from datetime import datetime, timezone from flask_caching import Cache import os @@ -223,8 +224,16 @@ def create_app(): from app.routes.app import bp as app_bp from app.routes.topics import bp as topics_bp + from app.routes.threads import bp as threads_bp + from app.routes.users import bp as users_bp + from app.routes.guides import bp as guides_bp + from app.routes.mod import bp as mod_bp app.register_blueprint(app_bp) app.register_blueprint(topics_bp) + app.register_blueprint(threads_bp) + app.register_blueprint(users_bp) + app.register_blueprint(guides_bp) + app.register_blueprint(mod_bp) @app.context_processor def inject_constants(): @@ -245,12 +254,18 @@ def create_app(): 'get_motds': MOTD.get_all, 'get_time_now': lambda: int(time.time()), 'is_logged_in': is_logged_in, + 'get_active_user': get_active_user, + 'get_post_url': get_post_url, } @app.template_filter('ts_datetime') def ts_datetime(ts, format): return datetime.utcfromtimestamp(ts or int(time.time())).strftime(format) + @app.template_filter('dict_to_query_string') + def d2q(d): + return dict_to_query_string(d) + @app.template_filter('pluralize') def pluralize(subject, num=1, singular = '', plural = 's'): if int(num) == 1: diff --git a/app/auth.py b/app/auth.py index a3fb19a..2e53294 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,6 +1,7 @@ from flask import session, flash -from .models import Sessions +from .models import Sessions, Users from argon2 import PasswordHasher +import secrets import time ph = PasswordHasher() @@ -15,9 +16,9 @@ def verify(expected, given): return False def is_logged_in(): - if "pyrom_session_key" not in session: + if 'pyrom_session_key' not in session: return False - sess = Sessions.find({"key": session["pyrom_session_key"]}) + sess = Sessions.find({'key': session['pyrom_session_key']}) if not sess: return False if sess.expires_at < int(time.time()): @@ -26,3 +27,17 @@ def is_logged_in(): # flash('Your session expired.;Please log in again.', InfoboxKind.INFO) return False return True + +def get_active_user(): + if not is_logged_in(): + return None + + sess = Sessions.find({'key': session['pyrom_session_key']}) + return Users.find({'id': sess.user_id}) + +def create_session(user_id): + return Sessions.create({ + 'key': secrets.token_hex(16), + 'user_id': user_id, + 'expires_at': int(time.time()) + (31 * 24 * 60 * 60), + }) diff --git a/app/models.py b/app/models.py index c6e0532..3054d81 100644 --- a/app/models.py +++ b/app/models.py @@ -131,35 +131,41 @@ class Topics(Model): order_clause = 'ORDER BY threads.is_stickied DESC, latest_post_created_at DESC' q = """ - SELECT - threads.id, threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied, - users.username AS started_by, - users.display_name AS started_by_display_name, - u.username AS latest_post_username, - u.display_name AS latest_post_display_name, - ph.content AS latest_post_content, - posts.created_at AS latest_post_created_at, - posts.id AS latest_post_id - FROM - threads - JOIN users ON users.id = threads.user_id - JOIN ( + WITH latest_posts AS ( SELECT - posts.thread_id, - posts.id, - posts.user_id, - posts.created_at, - posts.current_revision_id, - ROW_NUMBER() OVER (PARTITION BY posts.thread_id ORDER BY posts.created_at DESC) AS rn - FROM - posts - ) posts ON posts.thread_id = threads.id AND posts.rn = 1 - JOIN - post_history ph ON ph.id = posts.current_revision_id - JOIN - users u ON u.id = posts.user_id - WHERE - threads.topic_id = ? + thread_id, + id AS latest_post_id, + user_id AS latest_post_user_id, + created_at AS latest_post_created_at, + ROW_NUMBER() OVER (PARTITION BY thread_id ORDER BY created_at DESC) AS rn + FROM posts + ), + post_counts AS ( + SELECT + thread_id, + COUNT(*) AS posts_count + FROM posts + GROUP BY thread_id + ) + SELECT + threads.title, + threads.slug, + threads.created_at, + threads.is_locked, + threads.is_stickied, + starter.username AS started_by, + starter.display_name AS started_by_display_name, + latest_poster.username AS latest_post_username, + latest_poster.display_name AS latest_post_display_name, + latest_posts.latest_post_created_at, + latest_posts.latest_post_id, + COALESCE(post_counts.posts_count, 0) AS posts_count + FROM threads + JOIN users AS starter ON starter.id = threads.user_id + LEFT JOIN latest_posts ON latest_posts.thread_id = threads.id AND latest_posts.rn = 1 + LEFT JOIN users AS latest_poster ON latest_poster.id = latest_posts.latest_post_user_id + LEFT JOIN post_counts ON post_counts.thread_id = threads.id + WHERE threads.topic_id = ? """ + order_clause + ' LIMIT ? OFFSET ?' return db.query(q, self.id, per_page, (page - 1) * per_page) diff --git a/app/routes/guides.py b/app/routes/guides.py new file mode 100644 index 0000000..affede4 --- /dev/null +++ b/app/routes/guides.py @@ -0,0 +1,11 @@ +from flask import Blueprint + +bp = Blueprint('guides', __name__, url_prefix = '/guides/') + +@bp.get('/') +def index(): + return 'stub' + +@bp.get('/contact') +def contact(): + return 'stub' diff --git a/app/routes/mod.py b/app/routes/mod.py new file mode 100644 index 0000000..ac6f948 --- /dev/null +++ b/app/routes/mod.py @@ -0,0 +1,15 @@ +from flask import Blueprint + +bp = Blueprint('mod', __name__, url_prefix='/mod/') + +@bp.get('/') +def index(): + return 'stub' + +@bp.get('/topics/new') +def new_topic(): + return 'stub' + +@bp.get('/topics/sort') +def sort_topics(): + return 'stub' diff --git a/app/routes/threads.py b/app/routes/threads.py new file mode 100644 index 0000000..d88d3e0 --- /dev/null +++ b/app/routes/threads.py @@ -0,0 +1,11 @@ +from flask import Blueprint + +bp = Blueprint('threads', __name__, url_prefix='/threads/') + +@bp.get('/') +def thread(slug): + return 'stub' + +@bp.get('/new') +def new(): + return 'stub' diff --git a/app/routes/topics.py b/app/routes/topics.py index c214baf..b8d8f9a 100644 --- a/app/routes/topics.py +++ b/app/routes/topics.py @@ -1,6 +1,7 @@ -from flask import Blueprint, redirect, url_for, render_template +from flask import Blueprint, redirect, url_for, render_template, request, session -from ..models import Topics +from ..models import Topics, Threads +import math bp = Blueprint('topics', __name__, url_prefix = '/topics/') @@ -12,6 +13,15 @@ def all_topics(): @bp.get('/') def topic(slug): t = Topics.find({'slug': slug}) - if t: - return 'yes' - return 'no' + if not t: + return 'stub' + sort_by = request.args.get('sort_by', default=session.get('sort_by', default='activity')) + PER_PAGE = 10 + threads_count = Threads.count({'topic_id': t.id}) + page_count = max(1, math.ceil(threads_count / PER_PAGE)) + page = max(1, min(int(request.args.get('page', default=1)), page_count)) + return render_template('topics/topic.html', topic=t, threads=t.get_threads(PER_PAGE, page, sort_by), sort_by=sort_by, page=page, page_count=page_count) + +@bp.get('//feed.atom') +def feed(slug): + return 'stub' diff --git a/app/routes/users.py b/app/routes/users.py new file mode 100644 index 0000000..05d8e2e --- /dev/null +++ b/app/routes/users.py @@ -0,0 +1,38 @@ +from flask import Blueprint, redirect, url_for, render_template, request, session + +from ..auth import digest, verify, create_session +from ..models import Users + +bp = Blueprint('users', __name__, url_prefix='/users/') + +@bp.post('/log-in') +def log_in(): + user = Users.find({'username': request.form['username']}) + if not user: + return "no user" + if not verify(user.password_hash, request.form['password']): + return "no" + + sess = create_session(user.id) + session['pyrom_session_key'] = sess.key + return redirect(request.form['return_to']) + +@bp.get('/') +def user_page(username): + return 'stub' + +@bp.get('//settings') +def settings(username): + return 'stub' + +@bp.get('//inbox') +def inbox(username): + return 'stub' + +@bp.get('//bookmarks') +def bookmarks(username): + return 'stub' + +@bp.get('/sign-up') +def sign_up(): + return 'stub' diff --git a/app/templates/common/footer.html b/app/templates/common/footer.html index 71d6cf4..abae327 100644 --- a/app/templates/common/footer.html +++ b/app/templates/common/footer.html @@ -1,9 +1,7 @@ diff --git a/app/templates/common/macros.html b/app/templates/common/macros.html index 8e10f9b..f6a3751 100644 --- a/app/templates/common/macros.html +++ b/app/templates/common/macros.html @@ -1,3 +1,67 @@ {% macro timestamp(unix_ts) -%} {{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} ST {%- endmacro %} + +{% macro subheader(title, desc='') -%} +
+

{{title}}

+ {%- if desc -%}{{desc}}{%- endif -%} +
{% if caller %}{{- caller() -}}{% endif %}
+
+{%- endmacro %} + +{% macro pager(current_page, page_count, classes='', url='', args={}) -%} +{%- if args -%} +{#- remove the page query argument -#} +{%- set fargs = dict(args.items() | rejectattr(0, 'equalto', 'page')) -%} +{%- set url = url + (fargs | dict_to_query_string) + '&page=' -%} +{%- else -%} +{%- set url = url + '?page=' -%} +{%- endif -%} + +{%- if current_page == 0 -%} + {%- if page_count <= 3 -%} + {%- for i in range(page_count) -%} + {{i+1}} + {%- endfor -%} + {%- else -%} + 1 + 2 + + {{page_count - 1}} + {{page_count}} + {%- endif -%} +{%- else -%} + {%- set left_start = [2, current_page - 1] | max -%} + {%- set right_end = [page_count - 1, current_page + 1] | min -%} + + {%- if current_page != 1 -%} + 1 + {%- endif -%} + + {%- if left_start > 2 -%} + + {%- endif -%} + + {%- for i in range(left_start, current_page) -%} + {{i}} + {%- endfor -%} + + {%- if page_count > 0 -%} + + {%- endif -%} + + {%- for i in range(current_page + 1, right_end + 1) -%} + {{i}} + {%- endfor -%} + + {%- if right_end < page_count - 1 -%} + + {%- endif -%} + + {%- if page_count > 1 and current_page != page_count -%} + {{page_count}} + {%- endif -%} +{%- endif -%} + +{%- endmacro %} diff --git a/app/templates/common/topnav.html b/app/templates/common/topnav.html index d33b050..3c3f7de 100644 --- a/app/templates/common/topnav.html +++ b/app/templates/common/topnav.html @@ -1,15 +1,25 @@ diff --git a/app/templates/topics/topic.html b/app/templates/topics/topic.html new file mode 100644 index 0000000..81ce4ff --- /dev/null +++ b/app/templates/topics/topic.html @@ -0,0 +1,38 @@ +{% from 'common/macros.html' import timestamp, subheader, pager %} +{%- extends 'base.html' -%} +{%- block content -%} +{%- call() subheader(('Threads in "%s"' % topic.name), topic.description) -%} +
+ Actions + New thread + Subscribe via RSS +
+ + +
+
+{%- if get_active_user().is_mod() -%} +
+ Moderation actions +
+{%- endif -%} +{%- endcall -%} +{%- for thread in threads -%} +
+
+ {{thread.title}} +
    + {%- if thread.posts_count / 10 > 1 -%} + {{pager(0, (((thread.posts_count / 10) | round(0, 'ceil') )| int), 'flex-last', url=url_for('threads.thread', slug=thread.slug))}} + {%- endif -%} +
    + Started by {{thread.started_by_display_name if thread.started_by_display_name else thread.started_by}} on {{timestamp(thread.created_at)}} + {{thread.posts_count - 1}} {{'repl' | pluralize(thread.posts_count - 1, 'y', 'ies')}} + Latest reply by {{thread.latest_post_display_name if thread.latest_post_display_name else thread.latest_post_username}} on {{timestamp(thread.latest_post_created_at)}} +
    +{%- endfor -%} +{{pager(page, page_count, args=request.args)}} +{%- endblock -%} diff --git a/app/templates/topics/topics.html b/app/templates/topics/topics.html index 8096a33..4831062 100644 --- a/app/templates/topics/topics.html +++ b/app/templates/topics/topics.html @@ -1,6 +1,15 @@ -{% from 'common/macros.html' import timestamp %} +{% from 'common/macros.html' import timestamp, subheader %} {%- extends 'base.html' -%} {%- block content -%} +{%- call() subheader('All topics') -%} +{%- if get_active_user().is_mod() -%} +
    + Moderation actions + New topic + Sort topics +
    +{%- endif -%} +{%- endcall -%} {%- for topic in topics -%}
    diff --git a/app/util.py b/app/util.py new file mode 100644 index 0000000..8e823bf --- /dev/null +++ b/app/util.py @@ -0,0 +1,16 @@ +from flask import url_for +from .models import Posts, Threads + +def get_post_url(post_id, _anchor=False, external=False): + post = Posts.find({'id': post_id}) + if not post: + return '' + + thread = Threads.find({'id': post.thread_id}) + + anchor = None if not _anchor else f'post-{post_id}' + + return url_for('threads.thread', slug=thread.slug, after=post_id, _external=external, _anchor=anchor) + +def dict_to_query_string(d) -> str: + return '?' + '&'.join([f'{key}={str(value)}' for key, value in d.items()])