Compare commits

..

3 Commits

Author SHA1 Message Date
a12fd0a904
add commit to bottom nav 2025-06-30 22:34:19 +03:00
c22aa1036f
port mod app 2025-06-30 22:13:12 +03:00
453aeff95a
mostly implement topics app 2025-06-30 19:50:57 +03:00
12 changed files with 166 additions and 46 deletions

View File

@ -71,9 +71,11 @@ def create_app():
from app.routes.app import bp as app_bp from app.routes.app import bp as app_bp
from app.routes.topics import bp as topics_bp from app.routes.topics import bp as topics_bp
from app.routes.users import bp as users_bp from app.routes.users import bp as users_bp
from app.routes.mod import bp as mod_bp
app.register_blueprint(app_bp) app.register_blueprint(app_bp)
app.register_blueprint(topics_bp) app.register_blueprint(topics_bp)
app.register_blueprint(users_bp) app.register_blueprint(users_bp)
app.register_blueprint(mod_bp)
app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_SECURE'] = True
@ -83,10 +85,14 @@ def create_app():
@app.context_processor @app.context_processor
def inject_constants(): def inject_constants():
commit = ""
with open('.git/refs/heads/main') as f:
commit = f.read().strip()
return { return {
"InfoboxIcons": InfoboxIcons, "InfoboxIcons": InfoboxIcons,
"InfoboxHTMLClass": InfoboxHTMLClass, "InfoboxHTMLClass": InfoboxHTMLClass,
"InfoboxKind": InfoboxKind, "InfoboxKind": InfoboxKind,
"__commit": commit,
} }
@app.context_processor @app.context_processor

View File

@ -4,13 +4,12 @@ from flask import current_app
class DB: class DB:
def __init__(self): def __init__(self):
self._transaction_depth = 0
self._connection = None self._connection = None
@contextmanager @contextmanager
def _get_connection(self): def _get_connection(self):
if self._connection and self._transaction_depth > 0: if self._connection:
yield self._connection yield self._connection
return return
@ -21,48 +20,24 @@ class DB:
try: try:
yield conn yield conn
finally: finally:
if self._transaction_depth == 0: conn.close()
conn.close()
@contextmanager @contextmanager
def transaction(self): def transaction(self):
"""Transaction context.""" """Transaction context."""
self.begin() tr_connection = sqlite3.connect(current_app.config["DB_PATH"])
tr_connection.row_factory = sqlite3.Row
tr_connection.execute("PRAGMA FOREIGN_KEYS = 1")
tr_connection.execute("BEGIN")
try: try:
yield yield
self.commit() tr_connection.execute("COMMIT")
except Exception: except Exception:
self.rollback() tr_connection.execute("ROLLBACK")
raise raise
def begin(self):
"""Begins a new transaction."""
if self._transaction_depth == 0:
if not self._connection:
self._connection = sqlite3.connect(current_app.config["DB_PATH"])
self._connection.row_factory = sqlite3.Row
self._connection.execute("PRAGMA FOREIGN_KEYS = 1")
self._connection.execute("BEGIN")
self._transaction_depth += 1
def commit(self):
"""Commits the current transaction."""
if self._transaction_depth > 0:
self._transaction_depth -= 1
if self._transaction_depth == 0:
self._connection.commit()
def rollback(self):
"""Rolls back the current transaction."""
if self._transaction_depth > 0:
self._transaction_depth = 0
self._connection.rollback()
def query(self, sql, *args): def query(self, sql, *args):
"""Executes a query and returns a list of dictionaries.""" """Executes a query and returns a list of dictionaries."""
with self._get_connection() as conn: with self._get_connection() as conn:
@ -209,6 +184,13 @@ class Model:
return result["c"] if result else 0 return result["c"] if result else 0
@classmethod
def select(cls, sel = "*"):
qb = db.QueryBuilder(cls.table).select(sel)
result = qb.all()
return result if result else []
def update(self, data): def update(self, data):
qb = db.QueryBuilder(self.table)\ qb = db.QueryBuilder(self.table)\
.where({"id": self._data["id"]}) .where({"id": self._data["id"]})

33
app/routes/mod.py Normal file
View File

@ -0,0 +1,33 @@
from flask import (
Blueprint, render_template, request, redirect, url_for
)
from .users import login_required, mod_only
from ..models import Users
from ..db import db, DB
bp = Blueprint("mod", __name__, url_prefix = "/mod/")
@bp.get("/sort-topics")
@login_required
@mod_only("topics.all_topics")
def sort_topics():
topics = db.query("SELECT * FROM topics ORDER BY sort_order ASC")
return render_template("mod/sort-topics.html", topics = topics)
@bp.post("/sort-topics")
@login_required
@mod_only("topics.all_topics")
def sort_topics_post():
with db.transaction():
for topic_id, new_order in request.form.items():
db.execute("UPDATE topics SET sort_order = ? WHERE id = ?", new_order, topic_id)
return redirect(url_for(".sort_topics"))
@bp.get("/user-list")
@login_required
@mod_only("users.page", username = lambda: get_active_user().username)
def user_list():
users = Users.select()
return render_template("mod/user-list.html", users = users)

View File

@ -69,3 +69,44 @@ def topic(slug):
current_page = page, current_page = page,
page_count = page_count page_count = page_count
) )
@bp.get("/<slug>/edit")
@login_required
@mod_only(".topic", slug = lambda slug: slug)
def edit(slug):
topic = Topics.find({"slug": slug})
if not topic:
return "no"
return render_template("topics/edit.html", topic=topic)
@bp.post("/<slug>/edit")
@login_required
@mod_only(".topic", slug = lambda slug: slug)
def edit_post(slug):
topic = Topics.find({"slug": slug})
if not topic:
return "no"
topic.update({
"name": request.form.get('name', default = topic.name).strip(),
"description": request.form.get('description', default = topic.description),
"is_locked": int(request.form.get("is_locked", default = topic.is_locked)),
})
return redirect(url_for("topics.topic", slug=slug))
@bp.post("/<slug>/delete")
@login_required
@mod_only(".topic", slug = lambda slug: slug)
def delete(slug):
topic = Topics.find({"slug": slug})
if not topic:
return "no"
topic.delete()
flash("Topic deleted.", InfoboxKind.INFO)
return redirect(url_for("topics.all_topics"))

View File

@ -50,7 +50,7 @@ def redirect_if_logged_in(*args, **kwargs):
if is_logged_in(): if is_logged_in():
# resolve callables # resolve callables
processed_kwargs = { processed_kwargs = {
k: v() if callable(v) else v k: v(**view_kwargs) if callable(v) else v
for k, v in kwargs.items() for k, v in kwargs.items()
} }
endpoint = args[0] if args else processed_kwargs.get("endpoint") endpoint = args[0] if args else processed_kwargs.get("endpoint")
@ -81,7 +81,7 @@ def mod_only(*args, **kwargs):
if not get_active_user().is_mod(): if not get_active_user().is_mod():
# resolve callables # resolve callables
processed_kwargs = { processed_kwargs = {
k: v() if callable(v) else v k: v(**view_kwargs) if callable(v) else v
for k, v in kwargs.items() for k, v in kwargs.items()
} }
endpoint = args[0] if args else processed_kwargs.get("endpoint") endpoint = args[0] if args else processed_kwargs.get("endpoint")
@ -186,13 +186,6 @@ def inbox(username):
return "stub" return "stub"
@bp.get("/list")
@login_required
@mod_only(".page", username = lambda: get_active_user().username)
def user_list():
return "stub"
@bp.post("/log_out") @bp.post("/log_out")
def log_out(): def log_out():
pass pass

View File

@ -21,7 +21,7 @@
{% endwith %} {% endwith %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
<footer class="darkbg"> <footer class="darkbg">
<span>Pyrom commit</span> <span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/porom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
</footer> </footer>
<script src="/static/js/copy-code.js"></script> <script src="/static/js/copy-code.js"></script>
<script src="/static/js/ui.js"></script> <script src="/static/js/ui.js"></script>

View File

@ -14,7 +14,7 @@
<a href="{{ url_for("users.inbox", username = user.username) }}">Inbox</a> <a href="{{ url_for("users.inbox", username = user.username) }}">Inbox</a>
{% if user.is_mod() %} {% if user.is_mod() %}
&bullet; &bullet;
<a href="{{ url_for("users.user_list") }}">User list</a> <a href="{{ url_for("mod.user_list") }}">User list</a>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}

View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block content %}
<div class="darkbg settings-container">
<h1>Change topics order</h1>
<p>Drag topic titles to reoder them. Press submit when done. The topics will appear to users in the order set here.</p>
<form method="post" id=topics-container>
{% for topic in topics %}
<div draggable="true" class="draggable-topic" ondragover="dragOver(event)" ondragstart="dragStart(event)" ondragend="dragEnd()">
<div class="thread-title">{{ topic['name'] }}</div>
<div>{{ topic.description }}</div>
<input type="hidden" name="{{ topic['id'] }}" value="{{ topic['sort-order'] }}" class="topic-input">
</div>
{% endfor %}
<input type=submit value="Save order">
</form>
</div>
<script src="/static/js/sort-topics.js"></script>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<div class="darkbg settings-container">
<ul>
{% for user in users %}
<li><a href="{{url_for("users.page", username=user['username'])}}">{{user['username']}}</a>
{% endfor %}
</ul>
</div>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}creating a topic{% endblock %}
{% block content %}
<div class="darkbg settings-container">
<h1>Editing topic {{ topic['name'] }}</h1>
<form method="post">
<label for=name>Name</label>
<input type="text" name="name" id="name" required value="{{ topic['name'] }}"><br>
<label for="description">Description</label>
<textarea id="description" name="description" required rows=5>{{ topic['description'] }}</textarea><br>
<input type="submit" value="Save changes">
<a class="linkbutton warn" href={{ url_for("topics.topic", slug=topic['slug'] )}}>Cancel</a><br>
<i> Note: to preserve history, you cannot change the topic URL.</i>
</form>
</div>
{% endblock %}

View File

@ -7,6 +7,12 @@
<span>{{topic['description']}}</span> <span>{{topic['description']}}</span>
<div> <div>
{% if active_user and active_user.is_mod() %} {% if active_user and active_user.is_mod() %}
<a class="linkbutton" href="{{url_for("topics.edit", slug=topic['slug'])}}">Edit topic</a>
<form class="modform" method="post" action="{{url_for("topics.edit", slug=topic['slug']) }}">
<input type="hidden" name="is_locked" value="{{ (not topic.is_locked) | int }}">
<input class="warn" type="submit" id="lock" value="{{"Unlock topic" if topic['is_locked'] else "Lock topic"}}">
</form>
<button type="button" class="critical" id="topic-delete-dialog-open">Delete</button>
{% endif %} {% endif %}
</div> </div>
</nav> </nav>
@ -52,4 +58,17 @@
<nav id="bottomnav"> <nav id="bottomnav">
{{ pager(current_page = current_page, page_count = page_count) }} {{ pager(current_page = current_page, page_count = page_count) }}
</nav> </nav>
<dialog id="delete-dialog">
<div class="delete-dialog-inner">
Are you sure you want to delete this topic?
<span>
<button id=topic-delete-dialog-close>Cancel</button>
<button class="critical" form=topic-delete-form>Delete</button>
<form id="topic-delete-form" method="post" action="{{ url_for("topics.delete", slug = topic.slug) }}"></form>
</span>
</div>
</dialog>
<script src="/static/js/topic.js"></script>
{% endblock %} {% endblock %}

View File

@ -1,9 +1,11 @@
{% from 'common/macros.html' import timestamp %}
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<nav class="darkbg"> <nav class="darkbg">
<h1 class="thread-title">All topics</h1> <h1 class="thread-title">All topics</h1>
{% if active_user and active_user.is_mod() %} {% if active_user and active_user.is_mod() %}
<a class="linkbutton" href={{ url_for("topics.create") }}>Create new topic</a> <a class="linkbutton" href={{ url_for("topics.create") }}>Create new topic</a>
<a class="linkbutton" href={{ url_for("mod.sort_topics") }}>Sort topics</a>
{% endif %} {% endif %}
</nav> </nav>
{% if topic_list | length == 0 %} {% if topic_list | length == 0 %}
@ -16,12 +18,12 @@
{{ topic['description'] }} {{ topic['description'] }}
{% if topic['latest_thread_username'] %} {% if topic['latest_thread_username'] %}
<span> <span>
Latest thread: <a href="{{ url_for("threads.thread", slug=topic['latest_thread_slug'])}}">{{topic['latest_thread_title']}}</a> by <a href="{{url_for("users.page", username=topic['latest_thread_username'])}}">{{topic['latest_thread_username']}}</a> on ... Latest thread: <a href="{{ url_for("threads.thread", slug=topic['latest_thread_slug'])}}">{{topic['latest_thread_title']}}</a> by <a href="{{url_for("users.page", username=topic['latest_thread_username'])}}">{{topic['latest_thread_username']}}</a> on {{ timestamp(topic['latest_thread_created_at']) }}
</span> </span>
{% if topic['id'] in active_threads %} {% if topic['id'] in active_threads %}
{% with thread=active_threads[topic['id']] %} {% with thread=active_threads[topic['id']] %}
<span> <span>
Latest post in: <a href="{{ url_for("threads.thread", slug=thread['thread_slug'])}}">{{ thread['thread_title'] }}</a> by <a href="{{ url_for("users.page", username=thread['username'])}}">{{ thread['username'] }}</a> on ... Latest post in: <a href="{{ url_for("threads.thread", slug=thread['thread_slug'])}}">{{ thread['thread_title'] }}</a> by <a href="{{ url_for("users.page", username=thread['username'])}}">{{ thread['username'] }}</a> at <a href="">{{ timestamp(thread['post_created_at']) }}</a>
</span> </span>
{% endwith %} {% endwith %}
{% endif %} {% endif %}