start porting some topics stuff
This commit is contained in:
parent
9126ce4f61
commit
05cbc03e82
@ -80,6 +80,7 @@ def create_app():
|
|||||||
return {
|
return {
|
||||||
"InfoboxIcons": InfoboxIcons,
|
"InfoboxIcons": InfoboxIcons,
|
||||||
"InfoboxHTMLClass": InfoboxHTMLClass,
|
"InfoboxHTMLClass": InfoboxHTMLClass,
|
||||||
|
"InfoboxKind": InfoboxKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
|
104
app/models.py
104
app/models.py
@ -56,6 +56,110 @@ class Users(Model):
|
|||||||
class Topics(Model):
|
class Topics(Model):
|
||||||
table = "topics"
|
table = "topics"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_list(_cls):
|
||||||
|
q = """
|
||||||
|
SELECT
|
||||||
|
topics.id, topics.name, topics.slug, topics.description, topics.is_locked,
|
||||||
|
users.username AS latest_thread_username,
|
||||||
|
threads.title AS latest_thread_title,
|
||||||
|
threads.slug AS latest_thread_slug,
|
||||||
|
threads.created_at AS latest_thread_created_at
|
||||||
|
FROM
|
||||||
|
topics
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
*,
|
||||||
|
row_number() OVER (PARTITION BY threads.topic_id ORDER BY threads.created_at DESC) as rn
|
||||||
|
FROM
|
||||||
|
threads
|
||||||
|
) threads ON threads.topic_id = topics.id AND threads.rn = 1
|
||||||
|
LEFT JOIN
|
||||||
|
users on users.id = threads.user_id
|
||||||
|
ORDER BY
|
||||||
|
topics.sort_order ASC"""
|
||||||
|
return db.query(q)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_active_threads(cls):
|
||||||
|
q = """
|
||||||
|
WITH ranked_threads AS (
|
||||||
|
SELECT
|
||||||
|
threads.topic_id, threads.id AS thread_id, threads.title AS thread_title, threads.slug AS thread_slug,
|
||||||
|
posts.id AS post_id, posts.created_at AS post_created_at,
|
||||||
|
users.username,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY threads.topic_id ORDER BY posts.created_at DESC) AS rn
|
||||||
|
FROM
|
||||||
|
threads
|
||||||
|
JOIN
|
||||||
|
posts ON threads.id = posts.thread_id
|
||||||
|
LEFT JOIN
|
||||||
|
users ON posts.user_id = users.id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
topic_id,
|
||||||
|
thread_id, thread_title, thread_slug,
|
||||||
|
post_id, post_created_at,
|
||||||
|
username
|
||||||
|
FROM
|
||||||
|
ranked_threads
|
||||||
|
WHERE
|
||||||
|
rn = 1
|
||||||
|
ORDER BY
|
||||||
|
topic_id"""
|
||||||
|
|
||||||
|
active_threads_raw = db.query(q)
|
||||||
|
active_threads = {}
|
||||||
|
for thread in active_threads_raw:
|
||||||
|
active_threads[int(thread['topic_id'])] = {
|
||||||
|
'thread_title': thread['thread_title'],
|
||||||
|
'thread_slug': thread['thread_slug'],
|
||||||
|
'post_id': thread['post_id'],
|
||||||
|
'username': thread['username'],
|
||||||
|
'post_created_at': thread['post_created_at']
|
||||||
|
}
|
||||||
|
return active_threads
|
||||||
|
|
||||||
|
def get_threads(self, per_page, page, sort_by = "activity"):
|
||||||
|
order_clause = ""
|
||||||
|
if sort_by == "thread":
|
||||||
|
order_clause = "ORDER BY threads.is_stickied DESC, threads.created_at DESC"
|
||||||
|
else:
|
||||||
|
order_clause = "ORDER BY threads.is_stickied DESC, latest_post_created_at DESC"
|
||||||
|
|
||||||
|
q = """
|
||||||
|
SELECT
|
||||||
|
threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
|
||||||
|
users.username AS started_by,
|
||||||
|
u.username AS latest_post_username,
|
||||||
|
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 (
|
||||||
|
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 = ?
|
||||||
|
""" + order_clause + " LIMIT ? OFFSET ?"
|
||||||
|
|
||||||
|
return db.query(q, self.id, per_page, (page - 1) * per_page)
|
||||||
|
|
||||||
|
|
||||||
class Threads(Model):
|
class Threads(Model):
|
||||||
table = "threads"
|
table = "threads"
|
||||||
|
|
||||||
|
7
app/routes/threads.py
Normal file
7
app/routes/threads.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
bp = Blueprint("topics", __name__, url_prefix = "/threads/")
|
||||||
|
|
||||||
|
@bp.get("/<slug>")
|
||||||
|
def thread(slug):
|
||||||
|
return slug
|
@ -1,9 +1,71 @@
|
|||||||
from flask import Blueprint, render_template
|
from flask import (
|
||||||
from ..models import Users
|
Blueprint, render_template, request, redirect, url_for, flash, session
|
||||||
|
)
|
||||||
|
from .users import login_required, mod_only
|
||||||
|
from ..models import Users, Topics, Threads
|
||||||
|
from ..constants import InfoboxKind
|
||||||
|
from slugify import slugify
|
||||||
|
import time
|
||||||
|
import math
|
||||||
|
|
||||||
bp = Blueprint("topics", __name__, url_prefix = "/topics/")
|
bp = Blueprint("topics", __name__, url_prefix = "/topics/")
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.get("/")
|
||||||
def all_topics():
|
def all_topics():
|
||||||
admin = Users.find({"id": 1})
|
admin = Users.find({"id": 1})
|
||||||
return render_template("topics/topics.html", admin = admin)
|
return render_template("topics/topics.html", topic_list = Topics.get_list(), active_threads = Topics.get_active_threads())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/create")
|
||||||
|
@login_required
|
||||||
|
@mod_only(".all_topics")
|
||||||
|
def create():
|
||||||
|
return render_template("topics/create.html")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/create")
|
||||||
|
@login_required
|
||||||
|
@mod_only(".all_topics")
|
||||||
|
def create_post():
|
||||||
|
topic_name = request.form['name'].strip()
|
||||||
|
now = int(time.time())
|
||||||
|
slug = f"{slugify(topic_name)}-{now}"
|
||||||
|
|
||||||
|
topic_count = Topics.count()
|
||||||
|
topic = Topics.create({
|
||||||
|
"name": topic_name,
|
||||||
|
"description": request.form['description'],
|
||||||
|
"slug": slug,
|
||||||
|
"sort_order": topic_count + 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
flash("Topic created.", InfoboxKind.INFO)
|
||||||
|
return redirect(url_for("topics.topic", slug = slug))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/<slug>")
|
||||||
|
def topic(slug):
|
||||||
|
THREADS_PER_PAGE = 10
|
||||||
|
target_topic = Topics.find({
|
||||||
|
"slug": slug
|
||||||
|
})
|
||||||
|
if not target_topic:
|
||||||
|
return "no"
|
||||||
|
|
||||||
|
threads_count = Threads.count({
|
||||||
|
"topic_id": target_topic.id
|
||||||
|
})
|
||||||
|
|
||||||
|
sort_by = session.get('sort_by', default="activity")
|
||||||
|
|
||||||
|
page_count = max(math.ceil(threads_count / THREADS_PER_PAGE), 1)
|
||||||
|
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"topics/topic.html",
|
||||||
|
threads_list = target_topic.get_threads(THREADS_PER_PAGE, page, sort_by),
|
||||||
|
topic = target_topic,
|
||||||
|
current_page = page,
|
||||||
|
page_count = page_count
|
||||||
|
)
|
||||||
|
29
app/templates/common/pager.html
Normal file
29
app/templates/common/pager.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% macro pager(current_page, page_count) %}
|
||||||
|
{% set left_start = (1, current_page - 5) | max %}
|
||||||
|
{% set right_end = (page_count, current_page + 5) | min %}
|
||||||
|
|
||||||
|
<div class="pager">
|
||||||
|
<span>Page:</span>
|
||||||
|
{% if current_page > 5 %}
|
||||||
|
<a href="?page=1" class="pagebutton">1</a>
|
||||||
|
{% if left_start > 2 %}
|
||||||
|
<span class="currentpage">…</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% for i in range(left_start, current_page - 1) %}
|
||||||
|
<a href="?page={{i}}" class="pagebutton">{{i}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% if page_count > 0 %}
|
||||||
|
<span class="currentpage">{{current_page}}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% for i in range(current_page + 1, right_end) %}
|
||||||
|
<a href="?page={{i}}" class="pagebutton">{{i}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% if right_end < page_count %}
|
||||||
|
{% if right_end < page_count - 1 %}
|
||||||
|
<span class="currentpage">…</span>
|
||||||
|
{% endif %}
|
||||||
|
<a href="?page={{page_count}}" class="pagebutton">{{page_count}}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
14
app/templates/topics/create.html
Normal file
14
app/templates/topics/create.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}creating a topic{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="darkbg settings-container">
|
||||||
|
<h1>Create topic</h1>
|
||||||
|
<form method="post">
|
||||||
|
<label for=name>Name</label>
|
||||||
|
<input type="text" name="name" id="name" required><br>
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description" required rows=5></textarea><br>
|
||||||
|
<input type="submit" value="Create topic">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
55
app/templates/topics/topic.html
Normal file
55
app/templates/topics/topic.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{% from 'common/pager.html' import pager %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}browsing topic {{ topic['name'] }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<nav class="darkbg">
|
||||||
|
<h1 class="thread-title">All threads in "{{topic['name']}}"</h1>
|
||||||
|
<span>{{topic['description']}}</span>
|
||||||
|
<div>
|
||||||
|
{% if active_user and active_user.is_mod() %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% if topic['is_locked'] %}
|
||||||
|
{{ infobox("This topic is locked. Only moderators can create new threads.", InfoboxKind.INFO) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if threads_list | length == 0 %}
|
||||||
|
<p>There are no threads in this topic.</p>
|
||||||
|
{% else %}
|
||||||
|
{% for thread in threads_list %}
|
||||||
|
<div class="thread">
|
||||||
|
<div class="thread-sticky-container contain-svg">
|
||||||
|
{% if thread['is_stickied'] %}
|
||||||
|
<img src="/static/misc/sticky.svg">
|
||||||
|
<i>Stickied</i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="thread-info-container">
|
||||||
|
<span>
|
||||||
|
<span class="thread-title"><a href="{{ url_for("threads.thread", slug=thread['slug']) }}">{{thread['title']}}</a></span>
|
||||||
|
•
|
||||||
|
Started by <a href="{{ url_for("users.page", username=thread['started_by']) }}">{{ thread['started_by'] }}</a> on ...
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Latest post by <a href="{{ url_for("users.page", username=thread['latest_post_username']) }}">{{ thread['latest_post_username'] }}</a>
|
||||||
|
on ...
|
||||||
|
</span>
|
||||||
|
<span class="thread-info-post-preview">
|
||||||
|
{{ thread['latest_post_content'] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="thread-locked-container contain-svg">
|
||||||
|
{% if thread['is_locked'] %}
|
||||||
|
<img src="/static/misc/lock.svg">
|
||||||
|
<i>Locked</i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
<nav id="bottomnav">
|
||||||
|
{{ pager(current_page = current_page, page_count = page_count) }}
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
@ -2,6 +2,40 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<nav class="darkbg">
|
<nav class="darkbg">
|
||||||
<h1 class="thread-title">All topics</h1>
|
<h1 class="thread-title">All topics</h1>
|
||||||
<span>{{admin.is_default_avatar()}}</span>
|
{% if active_user and active_user.is_mod() %}
|
||||||
|
<a class="linkbutton" href={{ url_for("topics.create") }}>Create new topic</a>
|
||||||
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
{% if topic_list | length == 0 %}
|
||||||
|
<p>There are no topics.</p>
|
||||||
|
{% else %}
|
||||||
|
{% for topic in topic_list %}
|
||||||
|
<div class="topic">
|
||||||
|
<div class="topic-info-container">
|
||||||
|
<a class="thread-title" href="{{ url_for("topics.topic", slug=topic['slug']) }}">{{ topic['name'] }}</a>
|
||||||
|
{{ topic['description'] }}
|
||||||
|
{% if topic['latest_thread_username'] %}
|
||||||
|
<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 ...
|
||||||
|
</span>
|
||||||
|
{% if topic['id'] in active_threads %}
|
||||||
|
{% with thread=active_threads[topic['id']] %}
|
||||||
|
<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 ...
|
||||||
|
</span>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i>No threads yet.</i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="topic-locked-container contain-svg">
|
||||||
|
{% if topic['is_locked'] %}
|
||||||
|
<img src="/static/misc/lock.svg"></img>
|
||||||
|
<i>Locked</i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -463,7 +463,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.contain-svg:not(.full) > svg {
|
.contain-svg:not(.full) > svg, .contain-svg img {
|
||||||
height: 50%;
|
height: 50%;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
@ -2,3 +2,4 @@ flask
|
|||||||
argon2-cffi
|
argon2-cffi
|
||||||
wand
|
wand
|
||||||
dotenv
|
dotenv
|
||||||
|
python-slugify
|
||||||
|
@ -472,7 +472,7 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
&:not(.full) > svg {
|
&:not(.full) > svg, img {
|
||||||
height: 50%;
|
height: 50%;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user