Compare commits

...

11 Commits

7 changed files with 279 additions and 13 deletions

View File

@ -103,6 +103,6 @@ def create_app():
@app.template_filter("ts_datetime")
def ts_datetime(ts, format):
return datetime.utcfromtimestamp(ts).strftime(format)
return datetime.utcfromtimestamp(ts or int(time.time())).strftime(format)
return app

View File

@ -163,7 +163,31 @@ class Topics(Model):
class Threads(Model):
table = "threads"
def get_posts(self, limit, offset):
q = Posts.FULL_POSTS_QUERY + " WHERE posts.thread_id = ? ORDER BY posts.created_at ASC LIMIT ? OFFSET ?"
return db.query(q, self.id, limit, offset)
def locked(self):
return bool(self.is_locked)
def stickied(self):
return bool(self.is_stickied)
class Posts(Model):
FULL_POSTS_QUERY = """
SELECT
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup, users.signature_rendered, threads.slug AS thread_slug, threads.is_locked AS thread_is_locked
FROM
posts
JOIN
post_history ON posts.current_revision_id = post_history.id
JOIN
users ON posts.user_id = users.id
JOIN
threads ON posts.thread_id = threads.id
LEFT JOIN
avatars ON users.avatar_id = avatars.id"""
table = "posts"
class PostHistory(Model):

View File

@ -1,10 +1,12 @@
from flask import (
Blueprint, render_template, request, redirect, url_for
)
from .users import login_required, get_active_user
from ..models import Threads, Topics
from .users import login_required, mod_only, get_active_user, is_logged_in
from ..db import db
from ..models import Threads, Topics, Posts
from .posts import create_post
from slugify import slugify
import math
import time
bp = Blueprint("threads", __name__, url_prefix = "/threads/")
@ -12,7 +14,60 @@ bp = Blueprint("threads", __name__, url_prefix = "/threads/")
@bp.get("/<slug>")
def thread(slug):
return slug
POSTS_PER_PAGE = 10
thread = Threads.find({"slug": slug})
if not thread:
return "no"
post_count = Posts.count({"thread_id": thread.id})
page_count = max(math.ceil(post_count / POSTS_PER_PAGE), 1)
page = 1
after = request.args.get("after", default=None)
if after is not None:
after_id = int(after)
post_position = Posts.count([
("thread_id", "=", thread.id),
("id", "<=", after_id),
])
print(post_position)
page = math.ceil((post_position) / POSTS_PER_PAGE)
else:
page = max(1, min(page_count, int(request.args.get("page", default = 1))))
posts = thread.get_posts(POSTS_PER_PAGE, (page - 1) * POSTS_PER_PAGE)
topic = Topics.find({"id": thread.topic_id})
other_topics = Topics.select()
#TODO: subscription last seen
return render_template(
"threads/thread.html",
thread = thread,
current_page = page,
page_count = page_count,
posts = posts,
topic = topic,
topics = other_topics,
)
@bp.post("/<slug>")
@login_required
def reply(slug):
thread = Threads.find({"slug": slug})
if not thread:
return "no"
user = get_active_user()
if user.is_guest():
return "no"
if thread.locked() and not user.is_mod():
return "no"
post_content = request.form['post_content']
post = create_post(thread.id, user.id, post_content)
return redirect(url_for(".thread", slug=slug, after=post.id, _anchor="latest-post"))
@bp.get("/create")
@ -47,3 +102,23 @@ def create_form():
})
post = create_post(thread.id, user.id, post_content)
return redirect(url_for(".thread", slug = thread.slug))
@bp.post("/<slug>/lock")
@login_required
def lock(slug):
pass
@bp.post("/<slug>/sticky")
@login_required
@mod_only(".thread", slug = lambda slug: slug)
def sticky(slug):
pass
@bp.post("/<slug>/move")
@login_required
@mod_only(".thread", slug = lambda slug: slug)
def move(slug):
pass

View File

@ -1,6 +1,6 @@
{% macro pager(current_page, page_count) %}
{% set left_start = (1, current_page - 5) | max %}
{% set right_end = (page_count, current_page + 5) | min %}
{% 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 %}
@ -9,13 +9,13 @@
<span class="currentpage">&hellip;</span>
{% endif %}
{% endif %}
{% for i in range(left_start, current_page - 1) %}
{% for i in range(left_start, current_page) %}
<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) %}
{% for i in range(current_page + 1, right_end + 1) %}
<a href="?page={{i}}" class="pagebutton">{{i}}</a>
{% endfor %}
{% if right_end < page_count %}
@ -39,7 +39,7 @@
{% endmacro %}
{% macro timestamp(unix_ts) %}
<span class="timestamp" data-utc="{{ unix_ts }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} ST</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 %}
{% macro babycode_editor_component(ta_name, ta_placeholder="Post body", optional=False, prefill="") %}
@ -67,7 +67,7 @@
<div id="babycode-preview-container"></div>
</div>
</div>
<script src="/static/js/babycode-editor.js?v=1"></script>
<script src="/static/js/babycode-editor.js?v=2"></script>
{% endmacro %}
{% macro babycode_editor_form(ta_name, prefill = "", cancel_url="", endpoint="") %}
@ -88,3 +88,86 @@
</span>
</form>
{% endmacro %}
{% macro full_post(post, render_sig = True, is_latest = False, editing = False, active_user = None) %}
{% set postclass = "post" %}
{% if editing %}
{% set postclass = postclass + " editing" %}
{% endif %}
<div class=" {{ postclass }}" id="post-{{ post['id'] }}">
<div class="usercard">
<div class="usercard-inner">
<a href="{{ url_for("users.page", username=post['username']) }}" style="display: contents;">
<img src="{{ post['avatar_path'] }}" class="avatar">
</a>
<a href="{{ url_for("users.page", username=post['username']) }}" class="username-link">{{ post['username'] }}</a>
{% if post['status'] %}
<em class="user-status">{{ post['status'] }}</em>
{% endif %}
</div>
</div>
<div class="post-content-container" {{ "id=latest-post" if is_latest else "" }}>
<div class="post-info">
{% set post_permalink = url_for("threads.thread", slug = post['thread_slug'], after = post['id'], _anchor = ("post-" + (post['id'] | string))) %}
<a href="{{ post_permalink }}" title="Permalink"><i>
{% if (post['edited_at'] | int) > (post['created_at'] | int) %}
Edited on {{ timestamp(post['edited_at']) }}
{% else %}
Posted on {{ timestamp(post['edited_at']) }}
{% endif %}
</i></a>
<span>
{% set show_edit = false %}
{% if active_user %}
{% set show_edit = (active_user.id | string) == (post['user_id'] | string) and (not post['thread_is_locked'] or active_user.is_mod()) and not no_reply %}
{% endif %}
{% if show_edit %}
<a class="linkbutton" href="#TODO">Edit</a>
{% endif %}
{% set show_reply = true %}
{% if active_user and post['thread_is_locked'] and not active_user.is_mod() %}
{% set show_reply = false %}
{% elif active_user and active_user.is_guest() %}
{% set show_reply = false %}
{% elif editing %}
{% set show_reply = false %}
{% elif no_reply %}
{% set show_reply = false %}
{% endif %}
{% if show_reply %}
{% set qtext = "[url=%s]%s said:[/url]" | format(post_permalink, post['username']) %}
{% set reply_text = "%s\n[quote]%s[/quote]\n" | format(qtext, post['original_markup']) %}
<button value="{{ reply_text }}" class="reply-button">Quote</button>
{% endif %}
{% set show_delete = false %}
{% if active_user %}
{% set show_delete = (((post['user_id'] | string) == (active_user.id | string) and not post['thread_is_locked']) or active_user.is_mod()) and not no_reply %}
{% endif %}
{% if show_delete %}
<button class="critical post-delete-button" value="{{ post['id'] }}">Delete</button>
{% endif %}
</span>
</div>
<div class="post-content">
{% if not editing %}
<div class="post-inner">{{ post['content'] | safe }}</div>
{% if render_sig and post['signature_rendered'] %}
<div class="signature-container">
<hr>
{{ post['signature_rendered'] | safe }}
</div>
{% endif %}
{% else %}
{{ babycode_editor_form(cancel_url = post_permalink, prefill = post['original_markup'], ta_name = "new_content") }}
{% endif %}
</div>
</div>
</div>
{% endmacro %}

View File

@ -0,0 +1,71 @@
{% from 'common/macros.html' import pager, babycode_editor_form, full_post %}
{% extends "base.html" %}
{% block title %}{{ thread.title }}{% endblock %}
{% block content %}
{% set can_post = false %}
{% set can_lock = false %}
{% set can_subscribe = false %}
{% if active_user %}
{% set can_subscribe = true %}
{% set can_post = (not thread.is_locked and not active_user.is_guest()) or active_user.is_mod() %}
{% set can_lock = ((active_user.id | int) == (thread.user_id | int)) or active_user.is_mod() %}
{% endif %}
<main>
<nav class="darkbg">
<h1 class="thread-title">{{ thread.title }}</h1>
<span>Posted in <a href="{{ url_for("topics.topic", slug=topic.slug) }}">{{ topic.name }}</a>
{% if thread.is_stickied %}
&bullet; <i>stickied, so it's probably important</i>
{% endif %}
</span>
<div>
{% if can_subscribe %}
{% endif %}
{% if can_lock %}
<form class="modform" action="{{ url_for("threads.lock", slug=thread.slug) }}" method="post">
<input type=hidden value="{{ (not thread.is_locked) | int }}">
<input class="warn" type="submit" value="{{"Unlock thread" if thread.is_locked else "Lock thread"}}">
</form>
{% endif %}
{% if active_user.is_mod() %}
<form class="modform" action="{{ url_for("threads.sticky", slug=thread.slug) }}" method="post">
<input type=hidden value="{{ (not thread.is_stickied) | int }}">
<input class="warn" type="submit" value="{{"Unsticky thread" if thread.is_stickied else "Sticky thread"}}">
</form>
<form class="modform" action="{{ url_for("threads.move", slug=thread.slug) }}" method="post">
<label for="new_topic_id">Move to topic:</label>
<select style="width:200px;" id="new_topic_id" name="new_topic_id" autocomplete="off">
{% for topic in topics %}
<option value="{{ topic['id'] }}" {{ "selected disabled" if (thread.topic_id | string) == (topic['id'] | string) else "" }}>{{ topic['name'] }}</option>
{% endfor %}
</select>
<input class="warn" type="submit" value="Move thread">
</form>
{% endif %}
</div>
</nav>
{% for post in posts %}
{{ full_post(post = post, active_user = active_user, is_latest = loop.index == (posts | length)) }}
{% endfor %}
</main>
<nav id="bottomnav">
{{ pager(current_page = current_page, page_count = page_count) }}
</nav>
{% if can_post %}
<h1>Respond to "{{ thread.title }}"</h1>
{{ babycode_editor_form(ta_name = "post_content")}}
{% endif %}
<dialog id="delete-dialog">
<div class=delete-dialog-inner>
Are you sure you want to delete the highlighted post?
<span>
<button id=post-delete-dialog-close>Cancel</button>
<button class="critical" form=post-delete-form>Delete</button>
<form id="post-delete-form" method="post"></form>
</span>
</div>
</dialog>
<script src="/static/js/thread.js?v=1"></script>
{% endblock %}

View File

@ -45,10 +45,10 @@
</span>
<span>
Latest post by <a href="{{ url_for("users.page", username=thread['latest_post_username']) }}">{{ thread['latest_post_username'] }}</a>
on <a href="{{ url_for("threads.thread", slug=thread['slug']) }}">on {{ timestamp(thread['latest_post_created_at'] )}}</a>:
on <a href="{{ url_for("threads.thread", slug=thread['slug'], after=thread['latest_post_id']) }}">on {{ timestamp(thread['latest_post_created_at']) }}</a>:
</span>
<span class="thread-info-post-preview">
{{ thread['latest_post_content'] }}
{{ thread['latest_post_content'] | safe }}
</span>
</div>
<div class="thread-locked-container contain-svg">

View File

@ -4,6 +4,9 @@
ta.addEventListener("keydown", (e) => {
if(e.key === "Enter" && e.ctrlKey) {
// console.log(e.target.form)
if (inThread()) {
localStorage.removeItem(window.location.pathname);
}
e.target.form?.submit();
}
})
@ -18,7 +21,17 @@
localStorage.setItem(window.location.pathname, ta.value);
})
if (inThread()) {
const form = ta.closest('.post-edit-form');
console.log(ta.closest('.post-edit-form'));
if (form){
form.addEventListener("submit", () => {
localStorage.removeItem(window.location.pathname);
})
}
}
document.addEventListener("DOMContentLoaded", () => {
if (!inThread()) return;
const prevContent = localStorage.getItem(window.location.pathname);