start porting some topics stuff
This commit is contained in:
		@@ -80,6 +80,7 @@ def create_app():
 | 
			
		||||
        return {
 | 
			
		||||
            "InfoboxIcons": InfoboxIcons,
 | 
			
		||||
            "InfoboxHTMLClass": InfoboxHTMLClass,
 | 
			
		||||
            "InfoboxKind": InfoboxKind,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @app.context_processor
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										104
									
								
								app/models.py
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								app/models.py
									
									
									
									
									
								
							@@ -56,6 +56,110 @@ class Users(Model):
 | 
			
		||||
class Topics(Model):
 | 
			
		||||
    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):
 | 
			
		||||
    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 ..models import Users
 | 
			
		||||
from flask import (
 | 
			
		||||
    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.get("/")
 | 
			
		||||
def all_topics():
 | 
			
		||||
    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 %}
 | 
			
		||||
<nav class="darkbg">
 | 
			
		||||
  <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>
 | 
			
		||||
{% 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 %}
 | 
			
		||||
 
 | 
			
		||||
@@ -463,7 +463,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
.contain-svg:not(.full) > svg {
 | 
			
		||||
.contain-svg:not(.full) > svg, .contain-svg img {
 | 
			
		||||
  height: 50%;
 | 
			
		||||
  width: 50%;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,3 +2,4 @@ flask
 | 
			
		||||
argon2-cffi
 | 
			
		||||
wand
 | 
			
		||||
dotenv
 | 
			
		||||
python-slugify
 | 
			
		||||
 
 | 
			
		||||
@@ -472,7 +472,7 @@ input[type="text"], input[type="password"], textarea, select {
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  &:not(.full) > svg {
 | 
			
		||||
  &:not(.full) > svg, img {
 | 
			
		||||
    height: 50%;
 | 
			
		||||
    width: 50%;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user