start stubbing out endpoints
This commit is contained in:
@@ -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:
|
||||
|
||||
21
app/auth.py
21
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),
|
||||
})
|
||||
|
||||
@@ -131,35 +131,41 @@ class Topics(Model):
|
||||
order_clause = 'ORDER BY threads.is_stickied DESC, latest_post_created_at DESC'
|
||||
|
||||
q = """
|
||||
WITH latest_posts AS (
|
||||
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 (
|
||||
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
|
||||
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,
|
||||
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)
|
||||
|
||||
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/')
|
||||
|
||||
@@ -12,6 +13,15 @@ def all_topics():
|
||||
@bp.get('/<slug>')
|
||||
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('/<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">
|
||||
<span>Pyrom commit <a href="{{ "https://git.poto.cafe/yagich/pyrom/commit/" + __commit }}">{{ __commit[:8] }}</a></span>
|
||||
<ul class="horizontal">
|
||||
{#-
|
||||
<li><a href="#">Contact</a></li>
|
||||
<li><a href="#">Guides</a></li>
|
||||
-#}
|
||||
<li><a href="{{url_for('guides.contact')}}">Contact</a></li>
|
||||
<li><a href="{{url_for('guides.index')}}">Guides</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
@@ -1,3 +1,67 @@
|
||||
{% 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>
|
||||
{%- 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">
|
||||
<a class="site-title" href=#>Porom</a>
|
||||
<a class="site-title" href="/">Porom</a>
|
||||
<span>anti-social media</span>
|
||||
{%- 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 -%}
|
||||
<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="text" placeholder="Username">
|
||||
<input type="password" placeholder="Password">
|
||||
<input type="text" placeholder="Username" name="username" autocomplete="username" required>
|
||||
<input type="password" placeholder="Password" name="password" autocomplete="current-password" required>
|
||||
<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>
|
||||
{%- endif -%}
|
||||
</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' -%}
|
||||
{%- 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 -%}
|
||||
<div class="topic-info plank">
|
||||
<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