draw the rest of the owl

This commit is contained in:
2025-12-14 07:14:00 +03:00
parent 0898c56a51
commit d4e3d7cded
16 changed files with 258 additions and 22 deletions

View File

@@ -3,15 +3,15 @@ from dotenv import load_dotenv
from .models import Avatars, Users, PostHistory, Posts, MOTD, BadgeUploads from .models import Avatars, Users, PostHistory, Posts, MOTD, BadgeUploads
from .auth import digest from .auth import digest
from .routes.users import is_logged_in, get_active_user, get_prefers_theme from .routes.users import is_logged_in, get_active_user, get_prefers_theme
from .routes.threads import get_post_url
from .constants import ( from .constants import (
PermissionLevel, permission_level_string, PermissionLevel, permission_level_string,
InfoboxKind, InfoboxHTMLClass, InfoboxKind, InfoboxHTMLClass,
REACTION_EMOJI, MOTD_BANNED_TAGS, REACTION_EMOJI, MOTD_BANNED_TAGS,
SIG_BANNED_TAGS, STRICT_BANNED_TAGS, SIG_BANNED_TAGS, STRICT_BANNED_TAGS,
) )
from .lib.babycode import babycode_to_html, EMOJI, BABYCODE_VERSION from .lib.babycode import babycode_to_html, babycode_to_rssxml, EMOJI, BABYCODE_VERSION
from datetime import datetime from datetime import datetime, timezone
from flask_caching import Cache
import os import os
import time import time
import secrets import secrets
@@ -55,6 +55,18 @@ def reparse_babycode():
print('Re-parsing babycode, this may take a while...') print('Re-parsing babycode, this may take a while...')
from .db import db from .db import db
from .constants import MOTD_BANNED_TAGS from .constants import MOTD_BANNED_TAGS
post_histories_without_rss = PostHistory.findall([
('markup_language', '=', 'babycode'),
('content_rss', 'IS', None),
])
with db.transaction():
for ph in post_histories_without_rss:
ph.update({
'content_rss': babycode_to_rssxml(ph['original_markup']),
})
post_histories = PostHistory.findall([ post_histories = PostHistory.findall([
('markup_language', '=', 'babycode'), ('markup_language', '=', 'babycode'),
('format_version', 'IS NOT', BABYCODE_VERSION) ('format_version', 'IS NOT', BABYCODE_VERSION)
@@ -65,6 +77,7 @@ def reparse_babycode():
for ph in post_histories: for ph in post_histories:
ph.update({ ph.update({
'content': babycode_to_html(ph['original_markup']).result, 'content': babycode_to_html(ph['original_markup']).result,
'content_rss': babycode_to_rssxml(ph['original_markup']),
'format_version': BABYCODE_VERSION, 'format_version': BABYCODE_VERSION,
}) })
print('Re-parsing posts done.') print('Re-parsing posts done.')
@@ -125,6 +138,8 @@ def bind_default_badges(path):
}) })
cache = Cache()
def create_app(): def create_app():
app = Flask(__name__) app = Flask(__name__)
app.config['SITE_NAME'] = 'Pyrom' app.config['SITE_NAME'] = 'Pyrom'
@@ -133,6 +148,10 @@ def create_app():
app.config['USERS_CAN_INVITE'] = False app.config['USERS_CAN_INVITE'] = False
app.config['ADMIN_CONTACT_INFO'] = '' app.config['ADMIN_CONTACT_INFO'] = ''
app.config['GUIDE_DESCRIPTION'] = '' app.config['GUIDE_DESCRIPTION'] = ''
app.config['CACHE_TYPE'] = 'FileSystemCache'
app.config['CACHE_DEFAULT_TIMEOUT'] = 300
try: try:
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False) app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
except FileNotFoundError: except FileNotFoundError:
@@ -142,6 +161,7 @@ def create_app():
app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static") app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
app.debug = True app.debug = True
app.config["DB_PATH"] = "data/db/db.dev.sqlite" app.config["DB_PATH"] = "data/db/db.dev.sqlite"
app.config["SERVER_NAME"] = "localhost:8080"
load_dotenv() load_dotenv()
else: else:
app.config["DB_PATH"] = "data/db/db.prod.sqlite" app.config["DB_PATH"] = "data/db/db.prod.sqlite"
@@ -156,6 +176,13 @@ def create_app():
os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True) os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
os.makedirs(os.path.dirname(app.config["BADGES_UPLOAD_PATH"]), exist_ok = True) os.makedirs(os.path.dirname(app.config["BADGES_UPLOAD_PATH"]), exist_ok = True)
if app.config['CACHE_TYPE'] == 'FileSystemCache':
cache_dir = app.config.get('CACHE_DIR', 'data/_cached')
os.makedirs(cache_dir, exist_ok = True)
app.config['CACHE_DIR'] = cache_dir
cache.init_app(app)
css_dir = 'data/static/css/' css_dir = 'data/static/css/'
allowed_themes = [] allowed_themes = []
for f in os.listdir(css_dir): for f in os.listdir(css_dir):
@@ -229,10 +256,12 @@ def create_app():
@app.context_processor @app.context_processor
def inject_funcs(): def inject_funcs():
from .routes.threads import get_post_url
return { return {
'get_post_url': get_post_url, 'get_post_url': get_post_url,
'get_prefers_theme': get_prefers_theme, 'get_prefers_theme': get_prefers_theme,
'get_motds': MOTD.get_all, 'get_motds': MOTD.get_all,
'get_time_now': lambda: int(time.time()),
} }
@app.template_filter("ts_datetime") @app.template_filter("ts_datetime")
@@ -308,4 +337,8 @@ def create_app():
def fromjson(subject: str): def fromjson(subject: str):
return json.loads(subject) return json.loads(subject)
@app.template_filter('iso8601')
def unix_to_iso8601(subject: str):
return datetime.fromtimestamp(int(subject), timezone.utc).isoformat()
return app return app

View File

@@ -190,7 +190,7 @@ class RSSXMLRenderer(BabycodeRenderer):
def __init__(self, fragment=False): def __init__(self, fragment=False):
super().__init__(RSS_TAGS, VOID_TAGS, RSS_EMOJI, fragment) super().__init__(RSS_TAGS, VOID_TAGS, RSS_EMOJI, fragment)
def make_mention(self, element): def make_mention(self, e):
from ..models import Users from ..models import Users
from flask import url_for, current_app from flask import url_for, current_app
with current_app.test_request_context('/'): with current_app.test_request_context('/'):
@@ -410,10 +410,28 @@ def tag_code_rss(children, attr):
else: else:
return f'<pre><code>{children}</code></pre>' return f'<pre><code>{children}</code></pre>'
def tag_url_rss(children, attr):
if attr.startswith('/'):
from flask import current_app
uri = f"{current_app.config['PREFERRED_URL_SCHEME']}://{current_app.config['SERVER_NAME']}{attr}"
return f"<a href={uri}>{children}</a>"
return f"<a href={attr}>{children}</a>"
def tag_image_rss(children, attr):
if attr.startswith('/'):
from flask import current_app
uri = f"{current_app.config['PREFERRED_URL_SCHEME']}://{current_app.config['SERVER_NAME']}{attr}"
return f'<img src="{uri}" alt={children} />'
return f'<img src="{attr}" alt={children} />'
RSS_TAGS = { RSS_TAGS = {
**TAGS, **TAGS,
'img': lambda children, attr: f'<img src="{attr}" alt={children} />', 'img': tag_image_rss,
'spoiler': lambda children, attr: f'<details><summary>{attr or "Spoiler"}</summary>{children}</details>', 'url': tag_url_rss,
'spoiler': lambda children, attr: f'<details><summary>{attr or "Spoiler"} (click to reveal)</summary>{children}</details>',
'code': tag_code_rss, 'code': tag_code_rss,
'big': lambda children, attr: f'<span style="font-size: 1.2em">{children}</span>', 'big': lambda children, attr: f'<span style="font-size: 1.2em">{children}</span>',
@@ -424,6 +442,7 @@ VOID_TAGS = {
'lb': lambda attr: '[', 'lb': lambda attr: '[',
'rb': lambda attr: ']', 'rb': lambda attr: ']',
'@': lambda attr: '@', '@': lambda attr: '@',
'-': lambda attr: '-',
} }
# [img] is considered block for the purposes of collapsing whitespace, # [img] is considered block for the purposes of collapsing whitespace,
@@ -544,7 +563,7 @@ def babycode_ast(s: str, banned_tags=[]):
return elements return elements
def babycode_to_html(s: str, banned_tags=[], fragment=False): def babycode_to_html(s: str, banned_tags=[], fragment=False) -> BabycodeRenderResult:
""" """
transforms a string of babycode into html. transforms a string of babycode into html.
@@ -561,7 +580,7 @@ def babycode_to_html(s: str, banned_tags=[], fragment=False):
return r.render(ast) return r.render(ast)
def babycode_to_rssxml(s: str, banned_tags=[], fragment=False): def babycode_to_rssxml(s: str, banned_tags=[], fragment=False) -> str:
""" """
transforms a string of babycode into rss-compatible x/html. transforms a string of babycode into rss-compatible x/html.

10
app/lib/render_atom.py Normal file
View File

@@ -0,0 +1,10 @@
from flask import make_response, render_template, request
def render_atom_template(template, *args, **kwargs):
injects = {
**kwargs,
'__current_page': request.url,
}
r = make_response(render_template(template, *args, **injects))
r.mimetype = 'application/xml'
return r

View File

@@ -230,6 +230,38 @@ class Topics(Model):
return db.query(q, self.id, per_page, (page - 1) * per_page) return db.query(q, self.id, per_page, (page - 1) * per_page)
def get_threads_with_op_rss(self):
q = """
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,
ph.content_rss AS original_post_content,
posts.id AS original_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 ASC) 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 BY threads.created_at DESC"""
return db.query(q, self.id)
class Threads(Model): class Threads(Model):
table = "threads" table = "threads"
@@ -238,6 +270,10 @@ class Threads(Model):
q = Posts.FULL_POSTS_QUERY + " WHERE posts.thread_id = ? ORDER BY posts.created_at ASC 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) return db.query(q, self.id, limit, offset)
def get_posts_rss(self):
q = Posts.FULL_POSTS_QUERY + ' WHERE posts.thread_id = ?'
return db.query(q, self.id)
def locked(self): def locked(self):
return bool(self.is_locked) return bool(self.is_locked)
@@ -265,7 +301,7 @@ class Posts(Model):
SELECT SELECT
posts.id, posts.created_at, posts.id, posts.created_at,
post_history.content, post_history.edited_at, post_history.content, post_history.edited_at, post_history.content_rss,
users.username, users.display_name, users.status, users.username, users.display_name, users.status,
avatars.file_path AS avatar_path, posts.thread_id, avatars.file_path AS avatar_path, posts.thread_id,
users.id AS user_id, post_history.original_markup, users.id AS user_id, post_history.original_markup,

View File

@@ -1,7 +1,18 @@
from flask import Blueprint, redirect, url_for, render_template from flask import Blueprint, redirect, url_for, render_template
from app import cache
from datetime import datetime
bp = Blueprint("app", __name__, url_prefix = "/") bp = Blueprint("app", __name__, url_prefix = "/")
@bp.route("/") @bp.route("/")
def index(): def index():
return redirect(url_for("topics.all_topics")) return redirect(url_for("topics.all_topics"))
@bp.route("/cache-test")
def cache_test():
test_value = cache.get('test')
if test_value is None:
test_value = 'cached_value_' + str(datetime.now())
cache.set('test', test_value, timeout=10)
return f"set cache: {test_value}"
return f"cached: {test_value}"

View File

@@ -1,31 +1,35 @@
from flask import ( from flask import (
Blueprint, render_template, request, redirect, url_for, flash, Blueprint, render_template, request, redirect, url_for, flash,
abort, abort, current_app,
) )
from .users import login_required, mod_only, get_active_user, is_logged_in from .users import login_required, mod_only, get_active_user, is_logged_in
from ..db import db from ..db import db
from ..models import Threads, Topics, Posts, Subscriptions, Reactions from ..models import Threads, Topics, Posts, Subscriptions, Reactions
from ..constants import InfoboxKind from ..constants import InfoboxKind
from ..lib.render_atom import render_atom_template
from .posts import create_post from .posts import create_post
from slugify import slugify from slugify import slugify
from app import cache
import math import math
import time import time
bp = Blueprint("threads", __name__, url_prefix = "/threads/") bp = Blueprint("threads", __name__, url_prefix = "/threads/")
def get_post_url(post_id, _anchor=False): def get_post_url(post_id, _anchor=False, external=False):
post = Posts.find({'id': post_id}) post = Posts.find({'id': post_id})
if not post: if not post:
return "" return ""
thread = Threads.find({'id': post.thread_id}) thread = Threads.find({'id': post.thread_id})
res = url_for('threads.thread', slug=thread.slug, after=post_id) anchor = None if not _anchor else f'post-{post_id}'
if not _anchor:
return res
return f"{res}#post-{post_id}" return url_for('threads.thread', slug=thread.slug, after=post_id, _external=external, _anchor=anchor)
# if not _anchor:
# return res
# return f"{res}#post-{post_id}"
@bp.get("/<slug>") @bp.get("/<slug>")
@@ -80,9 +84,25 @@ def thread(slug):
is_subscribed = is_subscribed, is_subscribed = is_subscribed,
Reactions = Reactions, Reactions = Reactions,
unread_count = unread_count, unread_count = unread_count,
__feedlink = url_for('.thread_atom', slug=slug, _external=True),
__feedtitle = f'replies to {thread.title}',
) )
@bp.get("/<slug>/feed.atom")
@cache.cached(timeout=5 * 60, unless=lambda: current_app.config.get('DEBUG', False))
def thread_atom(slug):
thread = Threads.find({"slug": slug})
if not thread:
abort(404) # TODO throw an atom friendly 404
return
topic = Topics.find({'id': thread.topic_id})
posts = thread.get_posts_rss()
return render_atom_template('threads/thread.atom', thread=thread, topic=topic, posts=posts, get_post_url=get_post_url)
@bp.post("/<slug>") @bp.post("/<slug>")
@login_required @login_required
def reply(slug): def reply(slug):

View File

@@ -1,11 +1,13 @@
from flask import ( from flask import (
Blueprint, render_template, request, redirect, url_for, flash, session, Blueprint, render_template, request, redirect, url_for, flash, session,
abort, abort, current_app
) )
from .users import login_required, mod_only, get_active_user, is_logged_in from .users import login_required, mod_only, get_active_user, is_logged_in
from ..models import Users, Topics, Threads, Subscriptions from ..models import Users, Topics, Threads, Subscriptions
from ..constants import InfoboxKind from ..constants import InfoboxKind
from ..lib.render_atom import render_atom_template
from slugify import slugify from slugify import slugify
from app import cache
import time import time
import math import math
@@ -80,10 +82,27 @@ def topic(slug):
subscriptions = subscriptions, subscriptions = subscriptions,
topic = target_topic, topic = target_topic,
current_page = page, current_page = page,
page_count = page_count page_count = page_count,
__feedlink = url_for('.topic_atom', slug=slug, _external=True),
__feedtitle = f'latest threads in {target_topic.name}',
) )
@bp.get('/<slug>/feed.atom')
@cache.cached(timeout=10 * 60, unless=lambda: current_app.config.get('DEBUG', False))
def topic_atom(slug):
target_topic = Topics.find({
"slug": slug
})
if not target_topic:
abort(404) # TODO throw an atom friendly 404
return
threads_list = target_topic.get_threads_with_op_rss()
return render_atom_template('topics/topic.atom', threads_list=threads_list, target_topic=target_topic)
@bp.get("/<slug>/edit") @bp.get("/<slug>/edit")
@login_required @login_required
@mod_only(".topic", slug = lambda slug: slug) @mod_only(".topic", slug = lambda slug: slug)

20
app/templates/base.atom Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
{% if self.title() %}
<title>{% block title %}{% endblock %}</title>
{% else %}
<title>{{ config.SITE_NAME }}</title>
{% endif %}
{% if self.feed_updated() %}
<updated>{% block feed_updated %}{% endblock %}</updated>
{% else %}
<updated>{{ get_time_now() | iso8601 }}</updated>
{% endif %}
<id>{{ __current_page }}</id>
<link rel="self" href="{{ __current_page }}" />
<link href="{% block canonical_link %}{% endblock %}" />
{% if self.feed_author() %}
<author>{% block feed_author %}{% endblock %}</author>
{% endif %}
{% block content %}{% endblock %}
</feed>

View File

@@ -11,6 +11,9 @@
<link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}"> <link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}">
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<script src="{{ '/static/js/vnd/bitty-7.0.0-rc1.min.js' | cachebust }}" type="module"></script> <script src="{{ '/static/js/vnd/bitty-7.0.0-rc1.min.js' | cachebust }}" type="module"></script>
{% if __feedlink %}
<link rel="alternate" type="application/atom+xml" href="{{ __feedlink }}" title="{{ __feedtitle }}">
{% endif %}
</head> </head>
<body> <body>
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }}"> <bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }}">

View File

@@ -53,3 +53,9 @@
<path d="M6 18V14M6 14H8L13 17V7L8 10H5C3.89543 10 3 10.8954 3 12V12C3 13.1046 3.89543 14 5 14H6ZM17 7L19 5M17 17L19 19M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 18V14M6 14H8L13 17V7L8 10H5C3.89543 10 3 10.8954 3 12V12C3 13.1046 3.89543 14 5 14H6ZM17 7L19 5M17 17L19 19M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
{%- endmacro %} {%- endmacro %}
{% macro icn_rss(width=24) %}
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 11C9.41828 11 13 14.5817 13 19M5 5C12.732 5 19 11.268 19 19M7 18C7 18.5523 6.55228 19 6 19C5.44772 19 5 18.5523 5 18C5 17.4477 5.44772 17 6 17C6.55228 17 7 17.4477 7 18Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{% endmacro %}

View File

@@ -1,4 +1,4 @@
{% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark, icn_megaphone %} {% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark, icn_megaphone, icn_rss %}
{% macro pager(current_page, page_count) %} {% macro pager(current_page, page_count) %}
{% set left_start = [1, current_page - 5] | max %} {% set left_start = [1, current_page - 5] | max %}
{% set right_end = [page_count, current_page + 5] | min %} {% set right_end = [page_count, current_page + 5] | min %}
@@ -359,3 +359,11 @@
</div> </div>
</bitty-7-0> </bitty-7-0>
{% endmacro %} {% endmacro %}
{% macro rss_html_content(html) %}
<content type="html">{{ html }}</content>
{% endmacro %}
{% macro rss_button(feed) %}
<a class="linkbutton contain-svg inline icon rss-button" href="{{feed}}" title="it&#39;s actually atom, don&#39;t tell anyone &#59;&#41;">{{ icn_rss(20) }} Subscribe via RSS</a>
{% endmacro %}

View File

@@ -167,9 +167,18 @@
<a class="mention display me" href="#mentions" title="@your-username">Your display name</a> <a class="mention display me" href="#mentions" title="@your-username">Your display name</a>
<p>Mentioning a user does not notify them. It is simply a way to link to their profile in your posts.</p> <p>Mentioning a user does not notify them. It is simply a way to link to their profile in your posts.</p>
</section> </section>
<section class="guide-section">
{% set hr_example = "some section\n---\nanother section" %}
<h2 id="rule">Horizontal rules</h2>
<p>The special <code class="inline-code">---</code> markup inserts a horizontal separator, also known as a horizontal rule:</p>
{{ ("[code]%s[/code]" % hr_example) | babycode | safe }}
Will become
{{ hr_example | babycode(true) | safe}}
<p>Horizontal rules will always break the current paragraph.</p>
</section>
<section class="guide-section"> <section class="guide-section">
<h2 id="void-tags">Void tags</h2> <h2 id="void-tags">Void tags</h2>
<p>The special void tags <code class="inline-code">[lb]</code>, <code class="inline-code">[rb]</code>, and <code class="inline-code">[@]</code> will appear as the literal characters <code class="inline-code">[</code>, <code class="inline-code">]</code>, and <code class="inline-code">@</code> respectively. Unlike other tags, they are self-contained and have no closing equivalent.</p> <p>The special void tags <code class="inline-code">[lb]</code>, <code class="inline-code">[rb]</code>, <code class="inline-code">[-]</code> and <code class="inline-code">[@]</code> will appear as the literal characters <code class="inline-code">[</code>, <code class="inline-code">]</code>, and <code class="inline-code">@</code> respectively. Unlike other tags, they are self-contained and have no closing equivalent.</p>
<ul class="guide-list"> <ul class="guide-list">
{% set lbrb = "[color=red]This text will be red[/color]\n\n[lb]color=red[rb]This text won't be red[lb]/color[rb]" %} {% set lbrb = "[color=red]This text will be red[/color]\n\n[lb]color=red[rb]This text won't be red[lb]/color[rb]" %}
<li><code class="inline-code">[lb]</code> and <code class="inline-code">[rb]</code> allow you to use square brackets without them being interpreted as Babycode: <li><code class="inline-code">[lb]</code> and <code class="inline-code">[rb]</code> allow you to use square brackets without them being interpreted as Babycode:
@@ -178,6 +187,7 @@
{{ lbrb | babycode | safe }} {{ lbrb | babycode | safe }}
</li> </li>
<li>The <code class="inline-code">[@]</code> tag allows you to use the @ symbol without it being turned into a mention.</li> <li>The <code class="inline-code">[@]</code> tag allows you to use the @ symbol without it being turned into a mention.</li>
<li>The <code class="inline-code">[-]</code> tag allows you to use the - (dash) symbol without it being turned into a rule.</li>
</ul> </ul>
</section> </section>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends 'base.atom' %}
{% from 'common/macros.html' import rss_html_content %}
{% block title %}replies to {{thread.title}}{% endblock %}
{% block canonical_link %}{{url_for('threads.thread', slug=thread.slug, _external=true)}}{% endblock %}
{% block content %}
{% for post in posts %}
{% set post_url = get_post_url(post.id, _anchor=true, external=true) %}
<entry>
<title>Re: {{ thread.title }}</title>
<link href="{{ post_url }}"/>
<id>{{ post_url }}</id>
<updated>{{ post.edited_at | iso8601 }}</updated>
{{rss_html_content(post.content_rss)}}
<author>
<name>{{ post.display_name }} @{{ post.username }}</name>
</author>
</entry>
{% endfor %}
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% from 'common/macros.html' import pager, babycode_editor_form, full_post, bookmark_button %} {% from 'common/macros.html' import pager, babycode_editor_form, full_post, bookmark_button, rss_button %}
{% from 'common/icons.html' import icn_bookmark %} {% from 'common/icons.html' import icn_bookmark %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ thread.title }}{% endblock %} {% block title %}{{ thread.title }}{% endblock %}
@@ -53,6 +53,7 @@
<input class="warn" type="submit" value="Move thread"> <input class="warn" type="submit" value="Move thread">
</form> </form>
{% endif %} {% endif %}
{{ rss_button(url_for('threads.thread_atom', slug=thread.slug)) }}
</div> </div>
</nav> </nav>
{% for post in posts %} {% for post in posts %}

View File

@@ -0,0 +1,20 @@
{% extends 'base.atom' %}
{% from 'common/macros.html' import rss_html_content %}
{% block title %}latest threads in {{target_topic.name}}{% endblock %}
{% block canonical_link %}{{url_for('topics.topic', slug=target_topic.slug, _external=true)}}{% endblock %}
{% block content %}
<subtitle>{{ target_topic.description }}</subtitle>
{% for thread in threads_list %}
<entry>
<title>[new thread] {{ thread.title | escape }}</title>
<link href="{{ url_for('threads.thread', slug=thread.slug, _external=true)}}" />
<link rel="replies" type="application/atom+xml" href="{{ url_for('threads.thread_atom', slug=thread.slug, _external=true)}}" />
<id>{{ url_for('threads.thread', slug=thread.slug, _external=true)}}</id>
<updated>{{ thread.created_at | iso8601 }}</updated>
{{rss_html_content(thread.original_post_content)}}
<author>
<name>{{thread.started_by_display_name}} @{{ thread.started_by }}</name>
</author>
</entry>
{% endfor %}
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% from 'common/macros.html' import pager, timestamp, motd %} {% from 'common/macros.html' import pager, timestamp, motd, rss_button %}
{% from 'common/icons.html' import icn_lock, icn_sticky %} {% from 'common/icons.html' import icn_lock, icn_sticky %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}browsing topic {{ topic['name'] }}{% endblock %} {% block title %}browsing topic {{ topic['name'] }}{% endblock %}
@@ -6,7 +6,7 @@
<nav class="darkbg"> <nav class="darkbg">
<h1 class="thread-title">All threads in "{{topic['name']}}"</h1> <h1 class="thread-title">All threads in "{{topic['name']}}"</h1>
<span>{{topic['description']}}</span> <span>{{topic['description']}}</span>
<div> <div class="thread-actions">
{% if active_user %} {% if active_user %}
{% if not (topic['is_locked']) | int or active_user.is_mod() %} {% if not (topic['is_locked']) | int or active_user.is_mod() %}
<a class="linkbutton" href="{{ url_for("threads.create", topic_id=topic['id']) }}">New thread</a> <a class="linkbutton" href="{{ url_for("threads.create", topic_id=topic['id']) }}">New thread</a>
@@ -18,6 +18,7 @@
<input class="warn" type="submit" id="lock" value="{{"Unlock topic" if topic['is_locked'] else "Lock topic"}}"> <input class="warn" type="submit" id="lock" value="{{"Unlock topic" if topic['is_locked'] else "Lock topic"}}">
</form> </form>
<button type="button" class="critical" id="topic-delete-dialog-open">Delete</button> <button type="button" class="critical" id="topic-delete-dialog-open">Delete</button>
{{ rss_button(url_for('topics.topic_atom', slug=topic.slug)) }}
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>