start stubbing out endpoints
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
from flask import Flask, session, request, render_template
|
from flask import Flask, session, request, render_template
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from .models import Avatars, Users, PostHistory, Posts, MOTD, BadgeUploads, Sessions
|
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 (
|
from .constants import (
|
||||||
PermissionLevel, permission_level_string,
|
PermissionLevel, permission_level_string,
|
||||||
InfoboxKind, InfoboxHTMLClass,
|
InfoboxKind, InfoboxHTMLClass,
|
||||||
@@ -10,6 +10,7 @@ from .constants import (
|
|||||||
)
|
)
|
||||||
from .lib.babycode import babycode_to_html, babycode_to_rssxml, EMOJI, BABYCODE_VERSION
|
from .lib.babycode import babycode_to_html, babycode_to_rssxml, EMOJI, BABYCODE_VERSION
|
||||||
from .lib.exceptions import SiteNameMissingException
|
from .lib.exceptions import SiteNameMissingException
|
||||||
|
from .util import get_post_url, dict_to_query_string
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
import os
|
import os
|
||||||
@@ -223,8 +224,16 @@ 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.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(app_bp)
|
||||||
app.register_blueprint(topics_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
|
@app.context_processor
|
||||||
def inject_constants():
|
def inject_constants():
|
||||||
@@ -245,12 +254,18 @@ def create_app():
|
|||||||
'get_motds': MOTD.get_all,
|
'get_motds': MOTD.get_all,
|
||||||
'get_time_now': lambda: int(time.time()),
|
'get_time_now': lambda: int(time.time()),
|
||||||
'is_logged_in': is_logged_in,
|
'is_logged_in': is_logged_in,
|
||||||
|
'get_active_user': get_active_user,
|
||||||
|
'get_post_url': get_post_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.template_filter('ts_datetime')
|
@app.template_filter('ts_datetime')
|
||||||
def ts_datetime(ts, format):
|
def ts_datetime(ts, format):
|
||||||
return datetime.utcfromtimestamp(ts or int(time.time())).strftime(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')
|
@app.template_filter('pluralize')
|
||||||
def pluralize(subject, num=1, singular = '', plural = 's'):
|
def pluralize(subject, num=1, singular = '', plural = 's'):
|
||||||
if int(num) == 1:
|
if int(num) == 1:
|
||||||
|
|||||||
21
app/auth.py
21
app/auth.py
@@ -1,6 +1,7 @@
|
|||||||
from flask import session, flash
|
from flask import session, flash
|
||||||
from .models import Sessions
|
from .models import Sessions, Users
|
||||||
from argon2 import PasswordHasher
|
from argon2 import PasswordHasher
|
||||||
|
import secrets
|
||||||
import time
|
import time
|
||||||
|
|
||||||
ph = PasswordHasher()
|
ph = PasswordHasher()
|
||||||
@@ -15,9 +16,9 @@ def verify(expected, given):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def is_logged_in():
|
def is_logged_in():
|
||||||
if "pyrom_session_key" not in session:
|
if 'pyrom_session_key' not in session:
|
||||||
return False
|
return False
|
||||||
sess = Sessions.find({"key": session["pyrom_session_key"]})
|
sess = Sessions.find({'key': session['pyrom_session_key']})
|
||||||
if not sess:
|
if not sess:
|
||||||
return False
|
return False
|
||||||
if sess.expires_at < int(time.time()):
|
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)
|
# flash('Your session expired.;Please log in again.', InfoboxKind.INFO)
|
||||||
return False
|
return False
|
||||||
return True
|
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),
|
||||||
|
})
|
||||||
|
|||||||
@@ -131,35 +131,41 @@ class Topics(Model):
|
|||||||
order_clause = 'ORDER BY threads.is_stickied DESC, latest_post_created_at DESC'
|
order_clause = 'ORDER BY threads.is_stickied DESC, latest_post_created_at DESC'
|
||||||
|
|
||||||
q = """
|
q = """
|
||||||
SELECT
|
WITH latest_posts AS (
|
||||||
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 (
|
|
||||||
SELECT
|
SELECT
|
||||||
posts.thread_id,
|
thread_id,
|
||||||
posts.id,
|
id AS latest_post_id,
|
||||||
posts.user_id,
|
user_id AS latest_post_user_id,
|
||||||
posts.created_at,
|
created_at AS latest_post_created_at,
|
||||||
posts.current_revision_id,
|
ROW_NUMBER() OVER (PARTITION BY thread_id ORDER BY created_at DESC) AS rn
|
||||||
ROW_NUMBER() OVER (PARTITION BY posts.thread_id ORDER BY posts.created_at DESC) AS rn
|
FROM posts
|
||||||
FROM
|
),
|
||||||
posts
|
post_counts AS (
|
||||||
) posts ON posts.thread_id = threads.id AND posts.rn = 1
|
SELECT
|
||||||
JOIN
|
thread_id,
|
||||||
post_history ph ON ph.id = posts.current_revision_id
|
COUNT(*) AS posts_count
|
||||||
JOIN
|
FROM posts
|
||||||
users u ON u.id = posts.user_id
|
GROUP BY thread_id
|
||||||
WHERE
|
)
|
||||||
threads.topic_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 ?'
|
""" + order_clause + ' LIMIT ? OFFSET ?'
|
||||||
|
|
||||||
return db.query(q, self.id, per_page, (page - 1) * per_page)
|
return db.query(q, self.id, per_page, (page - 1) * per_page)
|
||||||
|
|||||||
11
app/routes/guides.py
Normal file
11
app/routes/guides.py
Normal file
@@ -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'
|
||||||
15
app/routes/mod.py
Normal file
15
app/routes/mod.py
Normal file
@@ -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'
|
||||||
11
app/routes/threads.py
Normal file
11
app/routes/threads.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('threads', __name__, url_prefix='/threads/')
|
||||||
|
|
||||||
|
@bp.get('/<slug>')
|
||||||
|
def thread(slug):
|
||||||
|
return 'stub'
|
||||||
|
|
||||||
|
@bp.get('/new')
|
||||||
|
def new():
|
||||||
|
return 'stub'
|
||||||
@@ -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/')
|
bp = Blueprint('topics', __name__, url_prefix = '/topics/')
|
||||||
|
|
||||||
@@ -12,6 +13,15 @@ def all_topics():
|
|||||||
@bp.get('/<slug>')
|
@bp.get('/<slug>')
|
||||||
def topic(slug):
|
def topic(slug):
|
||||||
t = Topics.find({'slug': slug})
|
t = Topics.find({'slug': slug})
|
||||||
if t:
|
if not t:
|
||||||
return 'yes'
|
return 'stub'
|
||||||
return 'no'
|
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('/<slug>/feed.atom')
|
||||||
|
def feed(slug):
|
||||||
|
return 'stub'
|
||||||
|
|||||||
38
app/routes/users.py
Normal file
38
app/routes/users.py
Normal file
@@ -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('/<username>')
|
||||||
|
def user_page(username):
|
||||||
|
return 'stub'
|
||||||
|
|
||||||
|
@bp.get('/<username>/settings')
|
||||||
|
def settings(username):
|
||||||
|
return 'stub'
|
||||||
|
|
||||||
|
@bp.get('/<username>/inbox')
|
||||||
|
def inbox(username):
|
||||||
|
return 'stub'
|
||||||
|
|
||||||
|
@bp.get('/<username>/bookmarks')
|
||||||
|
def bookmarks(username):
|
||||||
|
return 'stub'
|
||||||
|
|
||||||
|
@bp.get('/sign-up')
|
||||||
|
def sign_up():
|
||||||
|
return 'stub'
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
<footer class="plank secondary-bg bottom">
|
<footer class="plank secondary-bg bottom">
|
||||||
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
|
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
|
||||||
<ul class="horizontal">
|
<ul class="horizontal">
|
||||||
{#-
|
<li><a href="{{url_for('guides.contact')}}">Contact</a></li>
|
||||||
<li><a href="#">Contact</a></li>
|
<li><a href="{{url_for('guides.index')}}">Guides</a></li>
|
||||||
<li><a href="#">Guides</a></li>
|
|
||||||
-#}
|
|
||||||
</ul>
|
</ul>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,3 +1,67 @@
|
|||||||
{% macro timestamp(unix_ts) -%}
|
{% macro timestamp(unix_ts) -%}
|
||||||
<span class="timestamp" data-utc="{{ unix_ts }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></span>
|
<span class="timestamp" data-utc="{{ unix_ts }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></span>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro subheader(title, desc='') -%}
|
||||||
|
<div id="subheader" class="plank secondary-bg">
|
||||||
|
<h1 class="info">{{title}}</h1>
|
||||||
|
{%- if desc -%}<span>{{desc}}</span>{%- endif -%}
|
||||||
|
<div class="actions-group">{% if caller %}{{- caller() -}}{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
{%- 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 -%}
|
||||||
|
<span class="button-row {{classes}}">
|
||||||
|
{%- if current_page == 0 -%}
|
||||||
|
{%- if page_count <= 3 -%}
|
||||||
|
{%- for i in range(page_count) -%}
|
||||||
|
<a href="{{url}}{{i+1}}" class="linkbutton minimal">{{i+1}}</a>
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- else -%}
|
||||||
|
<a href="{{url}}1" class="linkbutton minimal">1</a>
|
||||||
|
<a href="{{url}}2" class="linkbutton minimal">2</a>
|
||||||
|
<button class="minimal" disabled>…</button>
|
||||||
|
<a href="{{url}}{{page_count - 1}}" class="linkbutton minimal">{{page_count - 1}}</a>
|
||||||
|
<a href="{{url}}{{page_count}}" class="linkbutton minimal">{{page_count}}</a>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- else -%}
|
||||||
|
{%- set left_start = [2, current_page - 1] | max -%}
|
||||||
|
{%- set right_end = [page_count - 1, current_page + 1] | min -%}
|
||||||
|
|
||||||
|
{%- if current_page != 1 -%}
|
||||||
|
<a href="{{url}}1" class="linkbutton minimal">1</a>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- if left_start > 2 -%}
|
||||||
|
<button class="minimal" disabled>…</button>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- for i in range(left_start, current_page) -%}
|
||||||
|
<a href="{{url}}{{i}}" class="linkbutton minimal">{{i}}</a>
|
||||||
|
{%- endfor -%}
|
||||||
|
|
||||||
|
{%- if page_count > 0 -%}
|
||||||
|
<button class="minimal" disabled>{{current_page}}</button>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- for i in range(current_page + 1, right_end + 1) -%}
|
||||||
|
<a href="{{url}}{{i}}" class="linkbutton minimal">{{i}}</a>
|
||||||
|
{%- endfor -%}
|
||||||
|
|
||||||
|
{%- if right_end < page_count - 1 -%}
|
||||||
|
<button class="minimal" disabled>…</button>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- if page_count > 1 and current_page != page_count -%}
|
||||||
|
<a href="{{url}}{{page_count}}" class="linkbutton minimal">{{page_count}}</a>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
||||||
|
</span>
|
||||||
|
{%- endmacro %}
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
<nav id="header" class="plank top">
|
<nav id="header" class="plank top">
|
||||||
<a class="site-title" href=#>Porom</a>
|
<a class="site-title" href="/">Porom</a>
|
||||||
<span>anti-social media</span>
|
<span>anti-social media</span>
|
||||||
{%- if is_logged_in() -%}
|
{%- if is_logged_in() -%}
|
||||||
no
|
{%- with user = get_active_user() -%}
|
||||||
|
<ul class="horizontal wrap">
|
||||||
|
<li class="mobile-fill-flex">Welcome, <a href="{{url_for('users.user_page', username=user.username)}}">{{ user.get_readable_name() }}</a></li>
|
||||||
|
<li><a class="linkbutton" href="{{url_for('users.settings', username=user.username)}}">Settings</a></li>
|
||||||
|
<li><a class="linkbutton" href="{{url_for('users.inbox', username=user.username)}}">Inbox</a></li>
|
||||||
|
<li><a class="linkbutton" href="{{url_for('users.bookmarks', username=user.username)}}">Bookmarks</a></li>
|
||||||
|
{% if user.is_mod() -%}
|
||||||
|
<li><a class="linkbutton" href="{{url_for('mod.index')}}">Moderation</a></li>
|
||||||
|
{%- endif %}
|
||||||
|
</ul>
|
||||||
|
{%- endwith -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
<form class="horizontal wrap">
|
<form class="horizontal wrap" method="POST" action="{{url_for('users.log_in')}}">
|
||||||
<input type="hidden" name="return_to" value="{{request.path}}">
|
<input type="hidden" name="return_to" value="{{request.path}}">
|
||||||
<input type="text" placeholder="Username">
|
<input type="text" placeholder="Username" name="username" autocomplete="username" required>
|
||||||
<input type="password" placeholder="Password">
|
<input type="password" placeholder="Password" name="password" autocomplete="current-password" required>
|
||||||
<input type="submit" value="Log in">
|
<input type="submit" value="Log in">
|
||||||
<a href="#" class="linkbutton">Register</a>
|
<a href="{{url_for('users.sign_up')}}" class="linkbutton">Sign up</a>
|
||||||
</form>
|
</form>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
38
app/templates/topics/topic.html
Normal file
38
app/templates/topics/topic.html
Normal file
@@ -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) -%}
|
||||||
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
|
<legend>Actions</legend>
|
||||||
|
<a href="{{url_for('threads.new', topic_id=topic.id)}}" class="linkbutton">New thread</a>
|
||||||
|
<a href="{{url_for('topics.feed', slug=topic.slug)}}" class="linkbutton rss">Subscribe via RSS</a>
|
||||||
|
<form method="GET">
|
||||||
|
<select name="sort_by">
|
||||||
|
<option value="activity"{% if sort_by == 'activity' %}selected{% endif %}>Sorted by activity</option>
|
||||||
|
<option value="thread" {% if sort_by == 'thread' %}selected{% endif %}>Sorted by newest</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="Sort">
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
|
{%- if get_active_user().is_mod() -%}
|
||||||
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
|
<legend>Moderation actions</legend>
|
||||||
|
</fieldset>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endcall -%}
|
||||||
|
{%- for thread in threads -%}
|
||||||
|
<div class="topic-info plank">
|
||||||
|
<div class="title-container">
|
||||||
|
<span class="info thread-title-counter"><a href="{{url_for('threads.thread', slug=thread.slug)}}">{{thread.title}}</a></span>
|
||||||
|
<ul class="horizontal"></ul>
|
||||||
|
{%- 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 -%}
|
||||||
|
</div>
|
||||||
|
<span>Started by <a href="{{url_for('users.user_page', username=thread.started_by)}}">{{thread.started_by_display_name if thread.started_by_display_name else thread.started_by}}</a> on {{timestamp(thread.created_at)}}</span>
|
||||||
|
<span>{{thread.posts_count - 1}} {{'repl' | pluralize(thread.posts_count - 1, 'y', 'ies')}}</span>
|
||||||
|
<span>Latest reply by <a href="{{get_post_url(thread.latest_post_id, _anchor=true)}}">{{thread.latest_post_display_name if thread.latest_post_display_name else thread.latest_post_username}} on {{timestamp(thread.latest_post_created_at)}}</a></span>
|
||||||
|
</div>
|
||||||
|
{%- endfor -%}
|
||||||
|
{{pager(page, page_count, args=request.args)}}
|
||||||
|
{%- endblock -%}
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
{% from 'common/macros.html' import timestamp %}
|
{% from 'common/macros.html' import timestamp, subheader %}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
|
{%- call() subheader('All topics') -%}
|
||||||
|
{%- if get_active_user().is_mod() -%}
|
||||||
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
|
<legend>Moderation actions</legend>
|
||||||
|
<a href="{{url_for('mod.new_topic')}}" class="linkbutton">New topic</a>
|
||||||
|
<a href="{{url_for('mod.sort_topics')}}" class="linkbutton">Sort topics</a>
|
||||||
|
</fieldset>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endcall -%}
|
||||||
{%- for topic in topics -%}
|
{%- for topic in topics -%}
|
||||||
<div class="topic-info plank">
|
<div class="topic-info plank">
|
||||||
<div class="title-container">
|
<div class="title-container">
|
||||||
|
|||||||
16
app/util.py
Normal file
16
app/util.py
Normal file
@@ -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()])
|
||||||
Reference in New Issue
Block a user