Compare commits
21 Commits
cd507ac25f
...
a5a3565496
| Author | SHA1 | Date | |
|---|---|---|---|
|
a5a3565496
|
|||
|
d74dd6c5f3
|
|||
|
648b310e13
|
|||
|
4bcea261b1
|
|||
|
4edc4f4650
|
|||
|
e670c176e8
|
|||
|
3870356ffa
|
|||
|
d5e627ed7f
|
|||
|
afdf182bd1
|
|||
|
ff2c6606f8
|
|||
|
f3acf64e6d
|
|||
|
20554d9c5c
|
|||
|
10ea1f03cd
|
|||
|
9a10f30634
|
|||
|
612d69c157
|
|||
|
286a3641eb
|
|||
|
b53556871f
|
|||
|
29f2318cba
|
|||
|
3d7188eb71
|
|||
|
ed4d4191d7
|
|||
|
66f381a434
|
@@ -197,14 +197,12 @@ def create_app():
|
|||||||
|
|
||||||
cache.init_app(app)
|
cache.init_app(app)
|
||||||
|
|
||||||
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.threads import bp as threads_bp
|
||||||
from app.routes.users import bp as users_bp
|
from app.routes.users import bp as users_bp
|
||||||
from app.routes.guides import bp as guides_bp
|
from app.routes.guides import bp as guides_bp
|
||||||
from app.routes.mod import bp as mod_bp
|
from app.routes.mod import bp as mod_bp
|
||||||
from app.routes.posts import bp as posts_bp
|
from app.routes.posts import bp as posts_bp
|
||||||
app.register_blueprint(app_bp)
|
|
||||||
app.register_blueprint(topics_bp)
|
app.register_blueprint(topics_bp)
|
||||||
app.register_blueprint(threads_bp)
|
app.register_blueprint(threads_bp)
|
||||||
app.register_blueprint(users_bp)
|
app.register_blueprint(users_bp)
|
||||||
@@ -319,6 +317,20 @@ def create_app():
|
|||||||
return {'error': 'not found'}, e.code
|
return {'error': 'not found'}, e.code
|
||||||
else:
|
else:
|
||||||
return render_template('common/404.html'), e.code
|
return render_template('common/404.html'), e.code
|
||||||
|
|
||||||
|
@app.errorhandler(403)
|
||||||
|
def _handle_403(e):
|
||||||
|
if request.path.startswith('/hyperapi/'):
|
||||||
|
return '<h1>forbiddedn</h1>', e.code
|
||||||
|
elif request.path.startswith('/api/'):
|
||||||
|
return {'error': 'forbidden'}, e.code
|
||||||
|
else:
|
||||||
|
return render_template('common/403.html'), e.code
|
||||||
|
|
||||||
|
@app.get('/')
|
||||||
|
def index():
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
#
|
#
|
||||||
# @app.errorhandler(413)
|
# @app.errorhandler(413)
|
||||||
# def _handle_413(e):
|
# def _handle_413(e):
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ def create_session(user_id, temporary=False):
|
|||||||
'expires_at': int(time.time()) + (expires_days * 24 * 60 * 60),
|
'expires_at': int(time.time()) + (expires_days * 24 * 60 * 60),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def revoke_session(user_id):
|
||||||
|
if not is_logged_in():
|
||||||
|
return
|
||||||
|
sess = Sessions.find({'key': session['pyrom_session_key']})
|
||||||
|
if not sess:
|
||||||
|
return
|
||||||
|
sess.delete()
|
||||||
|
session.clear()
|
||||||
|
|
||||||
def parse_username(username: str) -> Tuple[str, str]:
|
def parse_username(username: str) -> Tuple[str, str]:
|
||||||
"""first is the unmodified name/display name, second is username"""
|
"""first is the unmodified name/display name, second is username"""
|
||||||
if len(username) < 3:
|
if len(username) < 3:
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class DB:
|
|||||||
if self._transaction_depth == 0:
|
if self._transaction_depth == 0:
|
||||||
self._connection = None
|
self._connection = None
|
||||||
conn.close()
|
conn.close()
|
||||||
|
else:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ class Users(Model):
|
|||||||
WHERE users.id = ?"""
|
WHERE users.id = ?"""
|
||||||
return db.fetch_one(q, self.id)
|
return db.fetch_one(q, self.id)
|
||||||
|
|
||||||
|
def get_posts(self, per_page: int, page: int) -> list:
|
||||||
|
q = f'{Posts.FULL_POSTS_QUERY} WHERE posts.user_id = ? ORDER BY posts.created_at DESC LIMIT ? OFFSET ?'
|
||||||
|
return db.query(q, self.id, per_page, (page - 1) * per_page)
|
||||||
|
|
||||||
|
def get_started_threads(self, per_page: int, page: int) -> list:
|
||||||
|
q = f"""{Posts.FULL_POSTS_QUERY} WHERE threads.user_id = ? AND posts.created_at = (
|
||||||
|
SELECT MIN(p2.created_at)
|
||||||
|
FROM posts p2
|
||||||
|
WHERE p2.thread_id = posts.thread_id
|
||||||
|
) ORDER BY threads.created_at DESC LIMIT ? OFFSET ?"""
|
||||||
|
return db.query(q, self.id, per_page, (page - 1) * per_page)
|
||||||
|
|
||||||
def get_all_subscriptions(self):
|
def get_all_subscriptions(self):
|
||||||
q = """
|
q = """
|
||||||
SELECT threads.title AS thread_title, threads.slug AS thread_slug
|
SELECT threads.title AS thread_title, threads.slug AS thread_slug
|
||||||
@@ -61,14 +73,14 @@ class Users(Model):
|
|||||||
subscriptions.user_id = ?"""
|
subscriptions.user_id = ?"""
|
||||||
return db.query(q, self.id)
|
return db.query(q, self.id)
|
||||||
|
|
||||||
def can_post_to_topic(self, topic):
|
def can_post_to_thread_or_topic(self, thread_or_topic):
|
||||||
if self.is_guest():
|
if self.is_guest():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.is_mod():
|
if self.is_mod():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if topic['is_locked']:
|
if thread_or_topic['is_locked']:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -102,6 +114,9 @@ class Users(Model):
|
|||||||
def get_badges(self):
|
def get_badges(self):
|
||||||
return Badges.findall({'user_id': int(self.id)})
|
return Badges.findall({'user_id': int(self.id)})
|
||||||
|
|
||||||
|
def is_subscribed(self, thread_id):
|
||||||
|
return Subscriptions.count({'user_id': self.id, 'thread_id': thread_id}) > 0
|
||||||
|
|
||||||
|
|
||||||
class Topics(Model):
|
class Topics(Model):
|
||||||
table = 'topics'
|
table = 'topics'
|
||||||
@@ -282,6 +297,7 @@ class Posts(Model):
|
|||||||
users.id AS user_id, post_history.original_markup,
|
users.id AS user_id, post_history.original_markup,
|
||||||
users.signature_rendered, threads.slug AS thread_slug,
|
users.signature_rendered, threads.slug AS thread_slug,
|
||||||
threads.is_locked AS thread_is_locked, threads.title AS thread_title,
|
threads.is_locked AS thread_is_locked, threads.title AS thread_title,
|
||||||
|
topics.id AS topic_id, topics.name AS topic_name,
|
||||||
COALESCE(user_badges.badges_json, '[]') AS badges_json
|
COALESCE(user_badges.badges_json, '[]') AS badges_json
|
||||||
FROM
|
FROM
|
||||||
posts
|
posts
|
||||||
@@ -289,6 +305,8 @@ class Posts(Model):
|
|||||||
post_history ON posts.current_revision_id = post_history.id
|
post_history ON posts.current_revision_id = post_history.id
|
||||||
JOIN
|
JOIN
|
||||||
users ON posts.user_id = users.id
|
users ON posts.user_id = users.id
|
||||||
|
JOIN
|
||||||
|
topics ON threads.topic_id = topics.id
|
||||||
JOIN
|
JOIN
|
||||||
threads ON posts.thread_id = threads.id
|
threads ON posts.thread_id = threads.id
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
@@ -327,7 +345,8 @@ class Posts(Model):
|
|||||||
for mention in html_content.mentions:
|
for mention in html_content.mentions:
|
||||||
Mentions.create({
|
Mentions.create({
|
||||||
'revision_id': revision.id,
|
'revision_id': revision.id,
|
||||||
'mentioned_iser_id': mention['mentioned_iser_id'],
|
'mentioned_user_id': mention['mentioned_user_id'],
|
||||||
|
'original_mention_text': mention['mention_text'],
|
||||||
'start_index': mention['start'],
|
'start_index': mention['start'],
|
||||||
'end_index': mention['end'],
|
'end_index': mention['end'],
|
||||||
})
|
})
|
||||||
@@ -335,6 +354,32 @@ class Posts(Model):
|
|||||||
post.update({'current_revision_id': revision.id})
|
post.update({'current_revision_id': revision.id})
|
||||||
return post
|
return post
|
||||||
|
|
||||||
|
def edit(self, new_content: str, language: str = 'babycode'):
|
||||||
|
from .lib.babycode import babycode_to_html, babycode_to_rssxml, BABYCODE_VERSION
|
||||||
|
html_content = babycode_to_html(new_content)
|
||||||
|
rssxml_content = babycode_to_rssxml(new_content)
|
||||||
|
with db.transaction():
|
||||||
|
revision = PostHistory.create({
|
||||||
|
'post_id': self.id,
|
||||||
|
'content': html_content.result,
|
||||||
|
'content_rss': rssxml_content,
|
||||||
|
'is_initial_revision': False,
|
||||||
|
'original_markup': new_content,
|
||||||
|
'markup_language': language,
|
||||||
|
'format_version': BABYCODE_VERSION,
|
||||||
|
})
|
||||||
|
|
||||||
|
for mention in html_content.mentions:
|
||||||
|
Mentions.create({
|
||||||
|
'revision_id': revision.id,
|
||||||
|
'mentioned_user_id': mention['mentioned_user_id'],
|
||||||
|
'original_mention_text': mention['mention_text'],
|
||||||
|
'start_index': mention['start'],
|
||||||
|
'end_index': mention['end'],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.update({'current_revision_id': revision.id})
|
||||||
|
|
||||||
class PostHistory(Model):
|
class PostHistory(Model):
|
||||||
table = 'post_history'
|
table = 'post_history'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
from flask import Blueprint, redirect, url_for, render_template
|
|
||||||
bp = Blueprint('app', __name__, url_prefix = '/')
|
|
||||||
|
|
||||||
@bp.get('/')
|
|
||||||
def index():
|
|
||||||
return redirect(url_for('topics.all_topics'))
|
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
from flask import Blueprint, abort, redirect, url_for, request, render_template
|
from flask import Blueprint, abort, redirect, url_for, request, render_template, flash
|
||||||
|
from ..constants import InfoboxKind, PermissionLevel
|
||||||
from ..auth import is_logged_in, get_active_user, csrf_verified
|
from ..auth import is_logged_in, get_active_user, csrf_verified
|
||||||
from ..models import Topics, Threads
|
from ..models import Topics, Threads, Users
|
||||||
|
from slugify import slugify
|
||||||
|
from functools import wraps
|
||||||
|
import time
|
||||||
bp = Blueprint('mod', __name__, url_prefix='/mod/')
|
bp = Blueprint('mod', __name__, url_prefix='/mod/')
|
||||||
|
|
||||||
@bp.before_request
|
@bp.before_request
|
||||||
@@ -10,6 +14,14 @@ def mod_only():
|
|||||||
if not get_active_user().is_mod():
|
if not get_active_user().is_mod():
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
def admin_only(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not get_active_user().is_admin():
|
||||||
|
abort(403)
|
||||||
|
return view_func(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
@bp.get('/')
|
@bp.get('/')
|
||||||
def index():
|
def index():
|
||||||
return 'stub'
|
return 'stub'
|
||||||
@@ -39,9 +51,21 @@ def edit_topic_post(topic_id):
|
|||||||
topic = Topics.find({'id': topic_id})
|
topic = Topics.find({'id': topic_id})
|
||||||
if not topic:
|
if not topic:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
target_name = request.form.get('name').strip()
|
||||||
|
|
||||||
|
name_exists = Topics.count([
|
||||||
|
('lower(name)', '=', target_name.lower()),
|
||||||
|
('id', '!=', topic.id)
|
||||||
|
]) > 0
|
||||||
|
if name_exists:
|
||||||
|
flash(f'A topic named "{target_name}" already exists.', InfoboxKind.ERROR)
|
||||||
|
return redirect(url_for('.edit_topic', topic_id=topic_id))
|
||||||
|
|
||||||
topic.update({
|
topic.update({
|
||||||
'name': request.form.get('name').strip(),
|
'name': target_name,
|
||||||
'description': request.form.get('description').strip(),
|
'description': request.form.get('description').strip(),
|
||||||
|
'slug': slugify(target_name[:50]),
|
||||||
})
|
})
|
||||||
return redirect(url_for('topics.topic_by_id', topic_id=topic.id))
|
return redirect(url_for('topics.topic_by_id', topic_id=topic.id))
|
||||||
|
|
||||||
@@ -83,14 +107,62 @@ def sticky_thread(thread_id):
|
|||||||
@bp.post('/users/<int:user_id>/make-guest/')
|
@bp.post('/users/<int:user_id>/make-guest/')
|
||||||
@csrf_verified
|
@csrf_verified
|
||||||
def make_user_guest(user_id):
|
def make_user_guest(user_id):
|
||||||
return 'stub'
|
mod = get_active_user()
|
||||||
|
target_user = Users.find({'id': user_id})
|
||||||
|
if not target_user:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if target_user.is_admin() or target_user.is_system():
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
if int(target_user.permission) >= int(mod.permission):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
target_user.update({
|
||||||
|
'permission': PermissionLevel.GUEST.value,
|
||||||
|
'confirmed_on': None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return redirect(url_for('users.user_page', username=target_user.username))
|
||||||
|
|
||||||
@bp.post('/users/<int:user_id>/make-user/')
|
@bp.post('/users/<int:user_id>/make-user/')
|
||||||
@csrf_verified
|
@csrf_verified
|
||||||
def make_user_regular(user_id):
|
def make_user_regular(user_id):
|
||||||
return 'stub'
|
mod = get_active_user()
|
||||||
|
target_user = Users.find({'id': user_id})
|
||||||
|
if not target_user:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if target_user.is_admin() or target_user.is_system():
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
# mod -> regular user, abort if not admin
|
||||||
|
if int(target_user.permission) >= int(mod.permission):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
update_dict = {'permission': PermissionLevel.USER.value}
|
||||||
|
# set approved date if the user was guest
|
||||||
|
if target_user.is_guest():
|
||||||
|
update_dict['confirmed_on'] = int(time.time())
|
||||||
|
|
||||||
|
target_user.update(update_dict)
|
||||||
|
|
||||||
|
return redirect(url_for('users.user_page', username=target_user.username))
|
||||||
|
|
||||||
@bp.post('/users/<int:user_id>/make-mod/')
|
@bp.post('/users/<int:user_id>/make-mod/')
|
||||||
|
@admin_only
|
||||||
@csrf_verified
|
@csrf_verified
|
||||||
def make_user_mod(user_id):
|
def make_user_mod(user_id):
|
||||||
return 'stub'
|
mod = get_active_user()
|
||||||
|
target_user = Users.find({'id': user_id})
|
||||||
|
if not target_user:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if target_user.is_admin() or target_user.is_system():
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
if int(target_user.permission) >= int(mod.permission):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
target_user.update({'permission': PermissionLevel.MODERATOR.value})
|
||||||
|
return redirect(url_for('users.user_page', username=target_user.username))
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from flask import Blueprint, abort
|
from flask import Blueprint, abort, render_template, redirect, url_for, request, flash
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from ..auth import login_required, get_active_user
|
from ..auth import login_required, get_active_user
|
||||||
from ..models import Posts
|
from ..models import Posts, Threads, Topics
|
||||||
|
from ..util import get_post_url
|
||||||
|
from ..db import db
|
||||||
|
from ..constants import InfoboxKind
|
||||||
|
|
||||||
bp = Blueprint('posts', __name__, url_prefix='/posts/')
|
bp = Blueprint('posts', __name__, url_prefix='/posts/')
|
||||||
|
|
||||||
@@ -9,10 +12,11 @@ def ownership_required(view_func):
|
|||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
post = Posts.find({'id': kwargs.get('post_id', None)})
|
post = Posts.find({'id': kwargs.get('post_id', None)})
|
||||||
|
user = get_active_user()
|
||||||
if not post:
|
if not post:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if post.user_id != get_active_user().id:
|
if post.user_id != user.id:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
return view_func(*args, **kwargs)
|
return view_func(*args, **kwargs)
|
||||||
@@ -35,10 +39,97 @@ def ownership_or_mod_required(view_func):
|
|||||||
@login_required
|
@login_required
|
||||||
@ownership_required
|
@ownership_required
|
||||||
def edit(post_id):
|
def edit(post_id):
|
||||||
return 'stub'
|
post = Posts.find({'id': post_id})
|
||||||
|
if not post:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
thread = Threads.find({'id': post.thread_id})
|
||||||
|
if not thread:
|
||||||
|
# what?
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
user = get_active_user()
|
||||||
|
if thread.locked() and not user.is_mod():
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
thread_predicate = f'{Posts.FULL_POSTS_QUERY} WHERE posts.thread_id = ?'
|
||||||
|
|
||||||
|
context_prev_q = f'{thread_predicate} AND posts.created_at < ? ORDER BY posts.created_at DESC LIMIT 2'
|
||||||
|
context_next_q = f'{thread_predicate} AND posts.created_at > ? ORDER BY posts.created_at ASC LIMIT 2'
|
||||||
|
|
||||||
|
context_next = db.query(context_next_q, thread.id, post.created_at)
|
||||||
|
context_prev = db.query(context_prev_q, thread.id, post.created_at)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'posts/edit.html', post=post.get_full_post_view(),
|
||||||
|
context_next=context_next, context_prev=context_prev
|
||||||
|
)
|
||||||
|
|
||||||
|
@bp.post('/<int:post_id>/edit/')
|
||||||
|
@login_required
|
||||||
|
@ownership_required
|
||||||
|
def edit_post(post_id):
|
||||||
|
post = Posts.find({'id': post_id})
|
||||||
|
if not post:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
thread = Threads.find({'id': post.thread_id})
|
||||||
|
if not thread:
|
||||||
|
# what?
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
user = get_active_user()
|
||||||
|
if thread.locked() and not user.is_mod():
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
post.edit(request.form.get('babycode_content', ''))
|
||||||
|
|
||||||
|
return redirect(get_post_url(post.id, _anchor=True))
|
||||||
|
|
||||||
@bp.get('/<int:post_id>/delete/')
|
@bp.get('/<int:post_id>/delete/')
|
||||||
@login_required
|
@login_required
|
||||||
@ownership_or_mod_required
|
@ownership_or_mod_required
|
||||||
def delete(post_id):
|
def delete(post_id):
|
||||||
return 'stub'
|
post = Posts.find({'id': post_id})
|
||||||
|
if not post:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
thread = Threads.find({'id': post.thread_id})
|
||||||
|
if not thread:
|
||||||
|
# what?
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
user = get_active_user()
|
||||||
|
if thread.locked() and not user.is_mod():
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return render_template('posts/delete.html', post=post.get_full_post_view())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/<int:post_id>/delete/')
|
||||||
|
@login_required
|
||||||
|
@ownership_or_mod_required
|
||||||
|
def delete_post(post_id):
|
||||||
|
post = Posts.find({'id': post_id})
|
||||||
|
if not post:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
thread = Threads.find({'id': post.thread_id})
|
||||||
|
if not thread:
|
||||||
|
# what?
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
user = get_active_user()
|
||||||
|
if thread.locked() and not user.is_mod():
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
post.delete()
|
||||||
|
|
||||||
|
if Posts.count({'thread_id': thread.id}) == 0:
|
||||||
|
topic = Topics.find({'id': thread.topic_id})
|
||||||
|
thread.delete()
|
||||||
|
flash('Thread deleted.', InfoboxKind.INFO)
|
||||||
|
return redirect(url_for('topics.topic_by_id', topic_id=topic.id))
|
||||||
|
|
||||||
|
flash('Post deleted.', InfoboxKind.INFO)
|
||||||
|
return redirect(url_for('threads.thread_by_id', thread_id=thread.id))
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
from flask import Blueprint, redirect, url_for, render_template, request, abort
|
from flask import Blueprint, redirect, url_for, render_template, request, abort
|
||||||
|
from functools import wraps
|
||||||
from ..auth import login_required, get_active_user
|
from ..auth import login_required, get_active_user
|
||||||
from ..models import Threads, Posts, Topics, Users, Reactions
|
from ..models import Threads, Posts, Topics, Users, Reactions, Subscriptions
|
||||||
|
from ..util import get_form_checkbox, time_now
|
||||||
import math
|
import math
|
||||||
|
|
||||||
bp = Blueprint('threads', __name__, url_prefix='/threads/')
|
bp = Blueprint('threads', __name__, url_prefix='/threads/')
|
||||||
|
|
||||||
|
def ownership_or_mod_required(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
thread = Threads.find({'id': kwargs.get('thread_id', None)})
|
||||||
|
if not thread:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
if thread.user_id != get_active_user().id and not get_active_user().is_mod():
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return view_func(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
@bp.get('/<int:thread_id>/')
|
@bp.get('/<int:thread_id>/')
|
||||||
def thread_by_id(thread_id):
|
def thread_by_id(thread_id):
|
||||||
thread = Threads.find({'id': thread_id})
|
thread = Threads.find({'id': thread_id})
|
||||||
@@ -42,21 +57,102 @@ def thread(thread_id, slug):
|
|||||||
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
abort(404)
|
abort(404)
|
||||||
return render_template('threads/thread.html', thread=thread, posts=thread.get_posts(PER_PAGE, page), page=page, page_count=page_count, topic=topic, started_by=started_by, topics=Topics.get_list(), Reactions=Reactions)
|
posts = thread.get_posts(PER_PAGE, page)
|
||||||
|
last_post = posts[-1]
|
||||||
|
return render_template(
|
||||||
|
'threads/thread.html', thread=thread,
|
||||||
|
posts=posts, page=page,
|
||||||
|
page_count=page_count, topic=topic,
|
||||||
|
started_by=started_by, topics=Topics.get_list(),
|
||||||
|
Reactions=Reactions, last_post=last_post
|
||||||
|
)
|
||||||
|
|
||||||
@bp.post('/<int:thread_id>/reply/')
|
@bp.post('/<int:thread_id>/')
|
||||||
@login_required
|
@login_required
|
||||||
def reply(thread_id):
|
def reply(thread_id):
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
thread = Threads.find({'id': thread_id})
|
thread = Threads.find({'id': thread_id})
|
||||||
if not thread:
|
if not thread:
|
||||||
abort(404)
|
abort(404)
|
||||||
if thread.locked() and not user.is_mod():
|
if not user.can_post_to_thread_or_topic(thread):
|
||||||
# TODO: flash
|
|
||||||
return redirect(url_for('.thread_by_id', thread_id=thread_id))
|
return redirect(url_for('.thread_by_id', thread_id=thread_id))
|
||||||
post = Posts.new(user.id, thread.id, request.form.get('babycode_content'))
|
post = Posts.new(user.id, thread.id, request.form.get('babycode_content'))
|
||||||
|
if get_form_checkbox('subscribe'):
|
||||||
|
if not Subscriptions.find({'user_id': user.id, 'thread_id': thread.id}):
|
||||||
|
Subscriptions.create({
|
||||||
|
'user_id': user.id,
|
||||||
|
'thread_id': thread.id,
|
||||||
|
'last_seen': time_now(),
|
||||||
|
})
|
||||||
return redirect(url_for('.thread_by_id', thread_id=thread_id, after=post.id, _anchor=f'post-{post.id}'))
|
return redirect(url_for('.thread_by_id', thread_id=thread_id, after=post.id, _anchor=f'post-{post.id}'))
|
||||||
|
|
||||||
|
@bp.get('/<int:thread_id>/edit/')
|
||||||
|
@login_required
|
||||||
|
@ownership_or_mod_required
|
||||||
|
def edit(thread_id):
|
||||||
|
thread = Threads.find({'id': thread_id})
|
||||||
|
if not thread:
|
||||||
|
abort(404)
|
||||||
|
return render_template('threads/edit.html', thread=thread)
|
||||||
|
|
||||||
|
@bp.post('/<int:thread_id>/edit/')
|
||||||
|
@login_required
|
||||||
|
@ownership_or_mod_required
|
||||||
|
def edit_post(thread_id):
|
||||||
|
thread = Threads.find({'id': thread_id})
|
||||||
|
if not thread:
|
||||||
|
abort(404)
|
||||||
|
new_title = request.form.get('title', '').strip()
|
||||||
|
if not new_title:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if new_title != thread.title:
|
||||||
|
thread.update({'title': new_title})
|
||||||
|
|
||||||
|
return redirect(url_for('.thread_by_id', thread_id=thread_id))
|
||||||
|
|
||||||
|
@bp.post('/<int:thread_id>/subscribe/')
|
||||||
|
@login_required
|
||||||
|
def subscribe(thread_id):
|
||||||
|
thread = Threads.find({'id': thread_id})
|
||||||
|
if not thread:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
user = get_active_user()
|
||||||
|
last_post_id = request.form.get('last_post_id', None)
|
||||||
|
if last_post_id is None:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
if user.is_subscribed(thread_id):
|
||||||
|
return redirect(url_for('.thread_by_id', thread_id=thread_id, after=last_post_id))
|
||||||
|
|
||||||
|
Subscriptions.create({
|
||||||
|
'user_id': user.id,
|
||||||
|
'thread_id': thread_id,
|
||||||
|
'last_seen': request.form.get('last_post_timestamp', time_now())
|
||||||
|
})
|
||||||
|
|
||||||
|
return redirect(url_for('.thread_by_id', thread_id=thread_id, after=last_post_id))
|
||||||
|
|
||||||
|
@bp.post('/<int:thread_id>/unsubscribe/')
|
||||||
|
@login_required
|
||||||
|
def unsubscribe(thread_id):
|
||||||
|
thread = Threads.find({'id': thread_id})
|
||||||
|
if not thread:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
user = get_active_user()
|
||||||
|
last_post_id = request.form.get('last_post_id', None)
|
||||||
|
if last_post_id is None:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
subscription = Subscriptions.find({'user_id': user.id, 'thread_id': thread_id})
|
||||||
|
if not subscription:
|
||||||
|
return redirect(url_for('.thread_by_id', thread_id=thread_id, after=last_post_id))
|
||||||
|
|
||||||
|
subscription.delete()
|
||||||
|
return redirect(url_for('.thread_by_id', thread_id=thread_id, after=last_post_id))
|
||||||
|
|
||||||
@bp.get('/<int:thread_id>/feed.atom/')
|
@bp.get('/<int:thread_id>/feed.atom/')
|
||||||
def feed(thread_id):
|
def feed(thread_id):
|
||||||
return 'stub'
|
return 'stub'
|
||||||
@@ -84,19 +180,16 @@ def new_post():
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
if not user.can_post_to_topic(topic):
|
if not user.can_post_to_thread_or_topic(topic):
|
||||||
abort(404)
|
abort(403)
|
||||||
|
|
||||||
title = request.form.get('title')
|
title = request.form.get('title', '').strip()
|
||||||
if not title:
|
if not title:
|
||||||
abort(404)
|
abort(400)
|
||||||
|
|
||||||
if not title.strip():
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
title = title.strip()
|
title = title.strip()
|
||||||
|
|
||||||
content = request.form.get('babycode_content')
|
content = request.form.get('babycode_content')
|
||||||
|
|
||||||
thread = Threads.new(user.id, topic.id, title, content)
|
thread = Threads.new(user.id, topic.id, title, content)
|
||||||
return redirect(url_for('.thread', slug=thread.slug))
|
return redirect(url_for('.thread_by_id', thread_id=thread.id))
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from flask import Blueprint, redirect, url_for, render_template, request, session
|
from flask import Blueprint, redirect, url_for, render_template, request, session, abort
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ..auth import (
|
from ..auth import (
|
||||||
digest, verify, create_session,
|
digest, verify, create_session,
|
||||||
is_logged_in, parse_username, is_password_valid,
|
is_logged_in, parse_username, is_password_valid,
|
||||||
login_required
|
login_required, revoke_session, get_active_user
|
||||||
)
|
)
|
||||||
from ..models import Users
|
from ..models import Users, Posts, Reactions, Threads
|
||||||
from ..constants import PermissionLevel
|
from ..constants import PermissionLevel
|
||||||
from secrets import compare_digest as compare_timesafe
|
from secrets import compare_digest as compare_timesafe
|
||||||
|
import math
|
||||||
|
|
||||||
bp = Blueprint('users', __name__, url_prefix='/users/')
|
bp = Blueprint('users', __name__, url_prefix='/users/')
|
||||||
|
|
||||||
@@ -23,16 +24,24 @@ def redirect_if_logged_in(destination='topics.all_topics'):
|
|||||||
return wrapper
|
return wrapper
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
def redirect_to_own(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(username, *args, **kwargs):
|
||||||
|
user = get_active_user()
|
||||||
|
if username.lower() != user.username:
|
||||||
|
view_args = dict(request.view_args)
|
||||||
|
view_args.pop('username', None)
|
||||||
|
new_args = {**view_args, 'username': user.username}
|
||||||
|
return redirect(url_for(request.endpoint, **new_args))
|
||||||
|
return view_func(username, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@bp.get('/log-in/')
|
@bp.get('/log-in/')
|
||||||
@redirect_if_logged_in()
|
@redirect_if_logged_in()
|
||||||
def log_in():
|
def log_in():
|
||||||
return render_template('users/log_in.html')
|
return render_template('users/log_in.html')
|
||||||
|
|
||||||
@bp.post('/log-out/')
|
|
||||||
@login_required
|
|
||||||
def log_out():
|
|
||||||
return 'stub'
|
|
||||||
|
|
||||||
@bp.post('/log-in/')
|
@bp.post('/log-in/')
|
||||||
@redirect_if_logged_in()
|
@redirect_if_logged_in()
|
||||||
def log_in_post():
|
def log_in_post():
|
||||||
@@ -51,6 +60,12 @@ def log_in_post():
|
|||||||
session.permanent = True
|
session.permanent = True
|
||||||
return redirect(request.form.get('return_to', default=url_for('topics.all_topics')))
|
return redirect(request.form.get('return_to', default=url_for('topics.all_topics')))
|
||||||
|
|
||||||
|
@bp.post('/log-out/')
|
||||||
|
@login_required
|
||||||
|
def log_out():
|
||||||
|
revoke_session(get_active_user().id)
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
@bp.get('/sign-up/')
|
@bp.get('/sign-up/')
|
||||||
@redirect_if_logged_in()
|
@redirect_if_logged_in()
|
||||||
def sign_up():
|
def sign_up():
|
||||||
@@ -104,6 +119,7 @@ def sign_up_post():
|
|||||||
|
|
||||||
@bp.get('/<username>/')
|
@bp.get('/<username>/')
|
||||||
def user_page(username):
|
def user_page(username):
|
||||||
|
username = username.lower()
|
||||||
target_user = Users.find({'username': username})
|
target_user = Users.find({'username': username})
|
||||||
if not target_user:
|
if not target_user:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -111,25 +127,75 @@ def user_page(username):
|
|||||||
|
|
||||||
@bp.get('/<username>/posts/')
|
@bp.get('/<username>/posts/')
|
||||||
def posts(username):
|
def posts(username):
|
||||||
return 'stub'
|
username = username.lower()
|
||||||
|
if username == 'deleteduser':
|
||||||
|
abort(404)
|
||||||
|
target_user = Users.find({'username': username})
|
||||||
|
if not target_user:
|
||||||
|
abort(404)
|
||||||
|
PER_PAGE = 10
|
||||||
|
posts_count = Posts.count({'user_id': target_user.id})
|
||||||
|
page_count = max(1, math.ceil(posts_count / PER_PAGE))
|
||||||
|
page = 1
|
||||||
|
try:
|
||||||
|
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
||||||
|
except ValueError:
|
||||||
|
abort(404)
|
||||||
|
posts = target_user.get_posts(PER_PAGE, page)
|
||||||
|
return render_template(
|
||||||
|
'users/posts.html', posts=posts,
|
||||||
|
page=page, page_count=page_count,
|
||||||
|
target_user=target_user, Reactions=Reactions
|
||||||
|
)
|
||||||
|
|
||||||
@bp.get('/<username>/threads/')
|
@bp.get('/<username>/threads/')
|
||||||
def threads(username):
|
def threads(username):
|
||||||
return 'stub'
|
username = username.lower()
|
||||||
|
if username == 'deleteduser':
|
||||||
|
abort(404)
|
||||||
|
target_user = Users.find({'username': username})
|
||||||
|
if not target_user:
|
||||||
|
abort(404)
|
||||||
|
PER_PAGE = 10
|
||||||
|
threads_count = Threads.count({'user_id': target_user.id})
|
||||||
|
page_count = max(1, math.ceil(threads_count / PER_PAGE))
|
||||||
|
page = 1
|
||||||
|
try:
|
||||||
|
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
||||||
|
except ValueError:
|
||||||
|
abort(404)
|
||||||
|
threads = target_user.get_started_threads(PER_PAGE, page)
|
||||||
|
return render_template(
|
||||||
|
'users/threads.html', threads=threads,
|
||||||
|
page=page, page_count=page_count,
|
||||||
|
target_user=target_user, Reactions=Reactions
|
||||||
|
)
|
||||||
|
|
||||||
@bp.get('/<username>/comments/')
|
@bp.get('/<username>/comments/')
|
||||||
def comments(username):
|
def comments(username):
|
||||||
|
username = username.lower()
|
||||||
|
if username == 'deleteduser':
|
||||||
|
abort(404)
|
||||||
return 'stub'
|
return 'stub'
|
||||||
|
|
||||||
@bp.get('/<username>/settings/')
|
@bp.get('/<username>/settings/')
|
||||||
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def settings(username):
|
def settings(username):
|
||||||
|
username = username.lower()
|
||||||
return 'stub'
|
return 'stub'
|
||||||
|
|
||||||
@bp.get('/<username>/inbox/')
|
@bp.get('/<username>/inbox/')
|
||||||
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def inbox(username):
|
def inbox(username):
|
||||||
|
username = username.lower()
|
||||||
return 'stub'
|
return 'stub'
|
||||||
|
|
||||||
@bp.get('/<username>/bookmarks/')
|
@bp.get('/<username>/bookmarks/')
|
||||||
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def bookmarks(username):
|
def bookmarks(username):
|
||||||
|
username = username.lower()
|
||||||
return 'stub'
|
return 'stub'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{%- from 'common/macros.html' import infobox with context -%}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -13,6 +14,13 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{%- include 'common/topnav.html' -%}
|
{%- include 'common/topnav.html' -%}
|
||||||
|
{%- with messages = get_flashed_messages(with_categories=true) -%}
|
||||||
|
{%- if messages -%}
|
||||||
|
{%- for category, message in messages -%}
|
||||||
|
{{- infobox(message, category) -}}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endwith -%}
|
||||||
{%- block content -%}{%- endblock -%}
|
{%- block content -%}{%- endblock -%}
|
||||||
{%- include 'common/footer.html' -%}
|
{%- include 'common/footer.html' -%}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
8
app/templates/common/403.html
Normal file
8
app/templates/common/403.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{%- from 'common/macros.html' import subheader -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}Forbidden{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
{%- call() subheader('403 Forbidden') -%}
|
||||||
|
<span>You are not allowed to access this page or perform this action.</span>
|
||||||
|
{%- endcall -%}
|
||||||
|
{%- endblock -%}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
{%- block title -%}Not found{%- endblock -%}
|
{%- block title -%}Not found{%- endblock -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
{%- call() subheader('404 Not Found') -%}
|
{%- call() subheader('404 Not Found') -%}
|
||||||
<span>The requested URL was not found.</span>
|
<span>The requested page was not found.</span>
|
||||||
{%- endcall -%}
|
{%- endcall -%}
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|||||||
23
app/templates/common/icons.html
Normal file
23
app/templates/common/icons.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{%- macro icn_info(width=48) -%}
|
||||||
|
<img src="/static/icons/info.svg" alt="info" style="width: {{width}}px;">
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{%- macro icn_warn(width=48) -%}
|
||||||
|
<img src="/static/icons/warn.svg" alt="warning" style="width: {{width}}px;">
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{%- macro icn_error(width=48) -%}
|
||||||
|
<img src="/static/icons/error.svg" alt="error" style="width: {{width}}px;">
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{%- macro icn_bookmark(width=16) -%}
|
||||||
|
<img src="/static/icons/bookmark.svg" alt="bookmark" style="width: {{width}}px;">
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{%- macro icn_locked(width=16) -%}
|
||||||
|
<img src="/static/icons/locked.svg" title="Locked" alt="lock" style="width: {{width}}px;">
|
||||||
|
{%- endmacro -%}
|
||||||
|
|
||||||
|
{%- macro icn_stickied(width=16) -%}
|
||||||
|
<img src="/static/icons/sticky.svg" title="Stickied" alt="paper held by pushpin" style="width: {{width}}px;">
|
||||||
|
{%- endmacro -%}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
{%- from 'common/icons.html' import icn_info, icn_warn, icn_error, icn_bookmark -%}
|
||||||
|
|
||||||
{% 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>
|
<time datetime="{{ unix_ts | iso8601 }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></time>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro subheader(title, desc='') -%}
|
{% macro subheader(title, desc='') -%}
|
||||||
@@ -101,9 +103,9 @@
|
|||||||
<button type="button" class="minimal">•</button>
|
<button type="button" class="minimal">•</button>
|
||||||
<button type="button" class="minimal"><img src="/static/emoji/angry.png" class="emoji"></button>
|
<button type="button" class="minimal"><img src="/static/emoji/angry.png" class="emoji"></button>
|
||||||
</span>
|
</span>
|
||||||
<a href="##">babycode help</a>
|
|
||||||
</span>
|
</span>
|
||||||
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}}>{{ prefill }}</textarea>
|
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}}>{{ prefill }}</textarea>
|
||||||
|
<a href="##">babycode help</a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endcall -%}
|
{%- endcall -%}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
@@ -111,7 +113,7 @@
|
|||||||
{% macro full_post(
|
{% macro full_post(
|
||||||
post, render_sig=true, is_latest=false,
|
post, render_sig=true, is_latest=false,
|
||||||
show_toolbar=true, is_editing=false, thread=none,
|
show_toolbar=true, is_editing=false, thread=none,
|
||||||
show_reactions=true
|
show_reactions=true, show_thread=false, allow_reacting=true
|
||||||
) -%}
|
) -%}
|
||||||
{%- if is_logged_in() -%}
|
{%- if is_logged_in() -%}
|
||||||
{%- set can_delete = post.user_id == get_active_user().id or is_mod() -%}
|
{%- set can_delete = post.user_id == get_active_user().id or is_mod() -%}
|
||||||
@@ -140,11 +142,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<div class="plank even minimal secondary-bg no-shadow post-info">
|
<div class="plank even minimal secondary-bg no-shadow post-info">
|
||||||
<a href="{{get_post_url(post.id, _anchor=true)}}"><i>Posted on {{timestamp(post.created_at)}}</i></a>
|
<span>
|
||||||
|
<a href="{{get_post_url(post.id, _anchor=true)}}">
|
||||||
|
{%- if post.edited_at <= post.created_at -%}
|
||||||
|
<i>Posted on {{timestamp(post.created_at)}}</i>
|
||||||
|
{%- else -%}
|
||||||
|
<i>Edited on {{timestamp(post.edited_at)}}</i>
|
||||||
|
{%- endif -%}
|
||||||
|
</a>
|
||||||
|
{%- if show_thread -%}
|
||||||
|
<span> in thread <a href="{{url_for('threads.thread_by_id', thread_id=post.thread_id)}}"> {{post.thread_title}}</a></span>
|
||||||
|
{%- endif -%}
|
||||||
|
</span>
|
||||||
{%- if show_toolbar -%}
|
{%- if show_toolbar -%}
|
||||||
<span class="thread-actions">
|
<span class="thread-actions">
|
||||||
{%- if owns -%}
|
{%- if owns -%}
|
||||||
<a class="linkbutton" href="{{url_for('posts.edit', post_id=post.id)}}">Edit</a>
|
<a class="linkbutton" href="{{url_for('posts.edit', post_id=post.id, _anchor='babycode-content')}}">Edit</a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if can_reply -%}
|
{%- if can_reply -%}
|
||||||
<button disabled title="This feature requires JavaScript to be enabled.">Quote</button>
|
<button disabled title="This feature requires JavaScript to be enabled.">Quote</button>
|
||||||
@@ -152,31 +165,57 @@
|
|||||||
{%- if can_delete -%}
|
{%- if can_delete -%}
|
||||||
<a class="linkbutton critical" href="{{url_for('posts.delete', post_id=post.id)}}">Delete</a>
|
<a class="linkbutton critical" href="{{url_for('posts.delete', post_id=post.id)}}">Delete</a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<button disabled title="This feature requires JavaScript to be enabled.">Bookmark…</button>
|
<button disabled title="This feature requires JavaScript to be enabled.">{{icn_bookmark(24)}}Bookmark…</button>
|
||||||
</span>
|
</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
<div class="plank even no-shadow post-content-inner minimal">{{post.content | safe}}
|
<div class="plank even no-shadow post-content-inner minimal">
|
||||||
{%- if render_sig and post.signature_rendered -%}
|
{%- if not is_editing -%}
|
||||||
<aside class="post-signature">{{post.signature_rendered | safe}}</aside>
|
{{post.content | safe}}
|
||||||
|
{%- if render_sig and post.signature_rendered -%}
|
||||||
|
<aside class="post-signature">{{post.signature_rendered | safe}}</aside>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- else -%}
|
||||||
|
{{- babycode_editor_component(prefill=post.original_markup) -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
{%- if show_reactions -%}
|
|
||||||
<div class="plank even secondary-bg minimal no-shadow">
|
<div class="plank even secondary-bg minimal no-shadow">
|
||||||
<span class="button-row">
|
{%- if show_reactions -%}
|
||||||
{%- for reaction in Reactions.for_post(post.id) -%}
|
<span class="button-row">
|
||||||
{% set reactors = Reactions.get_users(post.id, reaction.reaction_text) | map(attribute='username') | list %}
|
{%- for reaction in Reactions.for_post(post.id) -%}
|
||||||
{% set reactors_trimmed = reactors[:10] %}
|
{% set reactors = Reactions.get_users(post.id, reaction.reaction_text) | map(attribute='username') | list %}
|
||||||
{% set reactors_str = reactors_trimmed | join (',\n') %}
|
{% set reactors_trimmed = reactors[:10] %}
|
||||||
{% if reactors | count > 10 %}
|
{% set reactors_str = reactors_trimmed | join (',\n') %}
|
||||||
{% set reactors_str = reactors_str + '\n...and many others' %}
|
{% if reactors | count > 10 %}
|
||||||
{% endif %}
|
{% set reactors_str = reactors_str + '\n...and many others' %}
|
||||||
{% set has_reacted = get_active_user() is not none and get_active_user().username in reactors %}
|
{% endif %}
|
||||||
<button disabled title="{{reactors_str}}" class="minimal {{'alt' if has_reacted else ''}}"><img src="/static/emoji/{{reaction.reaction_text}}.png">{{reaction.c}}</button>
|
{% set has_reacted = get_active_user() is not none and get_active_user().username in reactors %}
|
||||||
{%- endfor -%}
|
<button type="button" disabled title="{{reactors_str}}" class="minimal {{'alt' if has_reacted else ''}}"><img src="/static/emoji/{{reaction.reaction_text}}.png">{{reaction.c}}</button>
|
||||||
</span>
|
{%- endfor -%}
|
||||||
{%- if is_logged_in() -%}<button disabled title="This feature requires JavaScript to be enabled.">Add reaction</button>{%- endif -%}
|
</span>
|
||||||
|
{%- if is_logged_in() and allow_reacting -%}<button disabled title="This feature requires JavaScript to be enabled.">Add reaction</button>{%- endif -%}
|
||||||
|
{%- elif is_editing -%}
|
||||||
|
<input type="submit" value="Save">
|
||||||
|
<a href="{{get_post_url(post.id, _anchor=true)}}" class="linkbutton warn">Cancel</a>
|
||||||
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro infobox(message, kind=InfoboxKind.INFO) -%}
|
||||||
|
<div class="infobox plank top contain-svg horizontal {{InfoboxHTMLClass[kind]}}">
|
||||||
|
{%- if kind == InfoboxKind.INFO -%}
|
||||||
|
{{- icn_info() -}}
|
||||||
|
{%- elif kind == InfoboxKind.WARN -%}
|
||||||
|
{{- icn_warn() -}}
|
||||||
|
{%- elif kind == InfoboxKind.ERROR -%}
|
||||||
|
{{- icn_error() -}}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- set m = message.split(';', maxsplit=1) -%}
|
||||||
|
<strong>{{m[0]}}</strong>
|
||||||
|
{%- if m[1] -%}
|
||||||
|
{{m[1]}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block title -%}editing topic {{topic.name}}{%- endblock -%}
|
{%- block title -%}editing topic {{topic.name}}{%- endblock -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
{{subheader('Editing topic %s' % topic.name, 'To preserve history, the URL of the topic can not be changed.')}}
|
{{subheader('Editing topic %s' % topic.name)}}
|
||||||
<form class="plank primary-bg full-width" method="POST">
|
<form class="plank primary-bg full-width" method="POST">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input type="text" id="name" name="name" required value="{{topic.name}}">
|
<input type="text" id="name" name="name" required value="{{topic.name}}">
|
||||||
|
|||||||
16
app/templates/posts/delete.html
Normal file
16
app/templates/posts/delete.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{%- from 'common/macros.html' import subheader -%}
|
||||||
|
{%- from 'common/macros.html' import full_post with context -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}deleting a post{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
{%- call() subheader("Delete post", "Are you sure you want to delete this post? This action can not be undone.") -%}
|
||||||
|
<form method="POST">
|
||||||
|
<fieldset class="plank minimal even no-shadow thread-actions">
|
||||||
|
<legend>Please confirm</legend>
|
||||||
|
<a href="{{get_post_url(post.id, _anchor=true)}}" class="linkbutton">Cancel</a>
|
||||||
|
<input type="submit" value="Delete" class="critical">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{%- endcall -%}
|
||||||
|
<div class="post plank">{{- full_post(post=post, show_toolbar=false, show_reactions=false) -}}</div>
|
||||||
|
{%- endblock -%}
|
||||||
29
app/templates/posts/edit.html
Normal file
29
app/templates/posts/edit.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{%- from 'common/macros.html' import subheader -%}
|
||||||
|
{%- from 'common/macros.html' import full_post with context -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}editing a post{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
{%- set nav -%}
|
||||||
|
<a href="{{get_post_url(post.id, _anchor=true)}}">← Back to thread</a>
|
||||||
|
{%- endset -%}
|
||||||
|
{{ subheader("Editing your post", nav)}}
|
||||||
|
{%- for post in context_prev -%}
|
||||||
|
<div class="post plank">{{- full_post(post=post, show_toolbar=false, show_reactions=false) -}}</div>
|
||||||
|
{%- endfor -%}
|
||||||
|
<div class="plank secondary-bg context-explain">
|
||||||
|
<span>↑↑↑</span>
|
||||||
|
<i>Context</i>
|
||||||
|
<span>↑↑↑</span>
|
||||||
|
</div>
|
||||||
|
<form class="post plank" method="POST">
|
||||||
|
{{- full_post(post=post, is_editing=true, show_toolbar=false, show_reactions=false) -}}
|
||||||
|
</form>
|
||||||
|
<div class="plank secondary-bg context-explain">
|
||||||
|
<span>↓↓↓</span>
|
||||||
|
<i>Context</i>
|
||||||
|
<span>↓↓↓</span>
|
||||||
|
</div>
|
||||||
|
{%- for post in context_next -%}
|
||||||
|
<div class="post plank">{{- full_post(post=post, show_toolbar=false, show_reactions=false) -}}</div>
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endblock -%}
|
||||||
11
app/templates/threads/edit.html
Normal file
11
app/templates/threads/edit.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{%- from 'common/macros.html' import subheader, babycode_editor_component -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}editing thread "{{thread.title}}"{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
{{subheader('Rename thread "%s"' % thread.title, 'You can change the thread title here. To edit the OP of this thread, press the Edit button on the post instead.')}}
|
||||||
|
<form class="plank primary-bg full-width" method="POST">
|
||||||
|
<label for="title">New title</label>
|
||||||
|
<input type="text" id="title" name="title" required value="{{thread.title}}">
|
||||||
|
<input type="submit" value="Save">
|
||||||
|
</form>
|
||||||
|
{%- endblock -%}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<label for="topic">Topic</label>
|
<label for="topic">Topic</label>
|
||||||
<select name="topic_id" id="topic" autocomplete="off">
|
<select name="topic_id" id="topic" autocomplete="off">
|
||||||
{%- for topic in topics -%}
|
{%- for topic in topics -%}
|
||||||
<option value="{{topic.id}}" {{'selected' if selected_topic == topic.id else ''}} {{'disabled' if not get_active_user().can_post_to_topic(topic) else ''}}>{{topic.name}}{{ ' (locked)' if topic.locked() else ''}}</option>
|
<option value="{{topic.id}}" {{'selected' if selected_topic == topic.id else ''}} {{'disabled' if not get_active_user().can_post_to_thread_or_topic(topic) else ''}}>{{topic.name}}{{ ' (locked)' if topic.locked() else ''}}</option>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</select>
|
</select>
|
||||||
<label for="title">Title</label>
|
<label for="title">Title</label>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{%- from 'common/macros.html' import subheader, timestamp, pager, babycode_editor_component -%}
|
{%- from 'common/macros.html' import subheader, timestamp, pager, babycode_editor_component -%}
|
||||||
|
{%- from 'common/icons.html' import icn_bookmark -%}
|
||||||
{%- from 'common/macros.html' import full_post with context -%}
|
{%- from 'common/macros.html' import full_post with context -%}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block title -%}{{thread.title}}{%- endblock -%}
|
{%- block title -%}{{thread.title}}{%- endblock -%}
|
||||||
@@ -20,14 +21,24 @@
|
|||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
<legend>Actions</legend>
|
<legend>Actions</legend>
|
||||||
{%- if is_logged_in() -%}
|
{%- if is_logged_in() -%}
|
||||||
<button>Subscribe</button>
|
{%- if thread.user_id == get_active_user().id -%}
|
||||||
<button disabled title="This feature requires JavaScript to be enabled.">Bookmark…</button>
|
<a class="linkbutton" href="{{url_for('threads.edit', thread_id=thread.id)}}">Rename</a>
|
||||||
|
{%- endif -%}
|
||||||
|
<form action="{{url_for('threads.subscribe' if not get_active_user().is_subscribed(thread.id) else 'threads.unsubscribe', thread_id=thread.id)}}" method="POST">
|
||||||
|
<input type="hidden" name="last_post_timestamp" value="{{last_post.created_at}}">
|
||||||
|
<input type="hidden" name="last_post_id" value="{{last_post.id}}">
|
||||||
|
<input type="submit" value="{{'Subscribe' if not get_active_user().is_subscribed(thread.id) else 'Unsubscribe'}}">
|
||||||
|
</form>
|
||||||
|
<button disabled title="This feature requires JavaScript to be enabled.">{{icn_bookmark(24)}}Bookmark…</button>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<a href="{{url_for('threads.feed', thread_id=thread.id)}}" class="linkbutton rss">Subscribe via RSS</a>
|
<a href="{{url_for('threads.feed', thread_id=thread.id)}}" class="linkbutton rss">Subscribe via RSS</a>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{%- if is_mod() -%}
|
{%- if is_mod() -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
<legend>Moderation actions</legend>
|
<legend>Moderation actions</legend>
|
||||||
|
{%- if thread.user_id != get_active_user().id -%}
|
||||||
|
<a class="linkbutton warn" href="{{url_for('threads.edit', thread_id=thread.id)}}">Rename</a>
|
||||||
|
{%- endif -%}
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="lock" value="{{(not thread.locked()) | int}}">
|
<input type="hidden" name="lock" value="{{(not thread.locked()) | int}}">
|
||||||
<input type="hidden" name="sticky" value="{{(not thread.stickied()) | int}}">
|
<input type="hidden" name="sticky" value="{{(not thread.stickied()) | int}}">
|
||||||
@@ -63,7 +74,15 @@
|
|||||||
{{- pager(page, page_count) -}}
|
{{- pager(page, page_count) -}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
{%- if is_logged_in() -%}
|
<div id="new-post-toast" class="plank even contrast-bg hidden">
|
||||||
|
<span>New post in thread!</span>
|
||||||
|
<span class="notification-buttons">
|
||||||
|
<button>Dismiss</button>
|
||||||
|
<button>View post</button>
|
||||||
|
<button>Stop updates</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(thread) -%}
|
||||||
<form action="{{url_for('threads.reply', thread_id=thread.id)}}" method="POST" class="plank post-edit-form">
|
<form action="{{url_for('threads.reply', thread_id=thread.id)}}" method="POST" class="plank post-edit-form">
|
||||||
<h2 class="info">Reply to "{{thread.title}}"</h2>
|
<h2 class="info">Reply to "{{thread.title}}"</h2>
|
||||||
{{- babycode_editor_component() -}}
|
{{- babycode_editor_component() -}}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% from 'common/macros.html' import timestamp, subheader, pager %}
|
{% from 'common/macros.html' import timestamp, subheader, pager %}
|
||||||
|
{% from 'common/icons.html' import icn_locked, icn_stickied %}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block title -%}browsing topic {{topic.name}}{%- endblock -%}
|
{%- block title -%}browsing topic {{topic.name}}{%- endblock -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
{%- call() subheader(('Threads in "%s"' % topic.name), td) -%}
|
{%- call() subheader(('Threads in "%s"' % topic.name), td) -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
<legend>Actions</legend>
|
<legend>Actions</legend>
|
||||||
{%- if is_logged_in() and get_active_user().can_post_to_topic(topic) -%}
|
{%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(topic) -%}
|
||||||
<a href="{{url_for('threads.new', topic_id=topic.id)}}" class="linkbutton">New thread</a>
|
<a href="{{url_for('threads.new', topic_id=topic.id)}}" class="linkbutton">New thread</a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<a href="{{url_for('topics.feed', topic_id=topic.id)}}" class="linkbutton rss">Subscribe via RSS</a>
|
<a href="{{url_for('topics.feed', topic_id=topic.id)}}" class="linkbutton rss">Subscribe via RSS</a>
|
||||||
@@ -43,13 +44,21 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endcall -%}
|
{%- endcall -%}
|
||||||
{%- if threads | length == 0 -%}
|
{%- if threads | length == 0 -%}
|
||||||
<div class="plank"><p>There are no threads in this topic yet.{%- if is_logged_in() and get_active_user().can_post_to_topic(topic) %} Be the first to start a discussion!{%- endif -%}</p></div>
|
<div class="plank"><p>There are no threads in this topic yet.{%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(topic) %} Be the first to start a discussion!{%- endif -%}</p></div>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- for thread in threads -%}
|
{%- for thread in threads -%}
|
||||||
<div class="topic-info plank">
|
<div class="topic-info plank">
|
||||||
<div class="title-container">
|
<div class="title-container">
|
||||||
<span class="info thread-title-counter"><a href="{{url_for('threads.thread_by_id', thread_id=thread.id)}}">{{thread.title}}</a></span>
|
<span class="info thread-title-counter"><a href="{{url_for('threads.thread_by_id', thread_id=thread.id)}}">{{thread.title}}</a>
|
||||||
<ul class="horizontal"></ul>
|
</span>
|
||||||
|
<ul class="horizontal">
|
||||||
|
{%- if thread.is_locked -%}
|
||||||
|
<li>{{icn_locked(24)}}</li>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if thread.is_stickied -%}
|
||||||
|
<li>{{icn_stickied(24)}}</li>
|
||||||
|
{%- endif -%}
|
||||||
|
</ul>
|
||||||
{%- if thread.posts_count / 10 > 1 -%}
|
{%- if thread.posts_count / 10 > 1 -%}
|
||||||
{{pager(0, (((thread.posts_count / 10) | round(0, 'ceil') )| int), 'flex-last', url=url_for('threads.thread_by_id', thread_id=thread.id))}}
|
{{pager(0, (((thread.posts_count / 10) | round(0, 'ceil') )| int), 'flex-last', url=url_for('threads.thread_by_id', thread_id=thread.id))}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% from 'common/macros.html' import timestamp, subheader %}
|
{% from 'common/macros.html' import timestamp, subheader %}
|
||||||
|
{% from 'common/icons.html' import icn_locked %}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block content -%}
|
{%- block content -%}
|
||||||
{%- call() subheader('All topics') -%}
|
{%- call() subheader('All topics') -%}
|
||||||
@@ -14,6 +15,9 @@
|
|||||||
<div class="topic-info plank">
|
<div class="topic-info plank">
|
||||||
<div class="title-container">
|
<div class="title-container">
|
||||||
<a class="info" href="{{url_for('topics.topic_by_id', topic_id=topic.id)}}">{{topic.name}}</a>
|
<a class="info" href="{{url_for('topics.topic_by_id', topic_id=topic.id)}}">{{topic.name}}</a>
|
||||||
|
{%- if topic.is_locked -%}
|
||||||
|
{{icn_locked(24)}}
|
||||||
|
{%- endif -%}
|
||||||
</div>
|
</div>
|
||||||
<div>{{topic.description}}</div>
|
<div>{{topic.description}}</div>
|
||||||
<ul class="horizontal">
|
<ul class="horizontal">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% from 'common/macros.html' import infobox with context %}
|
||||||
{% from 'common/macros.html' import subheader %}
|
{% from 'common/macros.html' import subheader %}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block title -%}log in{%- endblock -%}
|
{%- block title -%}log in{%- endblock -%}
|
||||||
@@ -7,9 +8,7 @@ Welcome back! No account yet? <a href="{{url_for('users.sign_up')}}">Sign up</a>
|
|||||||
{%- endset -%}
|
{%- endset -%}
|
||||||
{{ subheader('Log in', welcome)}}
|
{{ subheader('Log in', welcome)}}
|
||||||
{%- if request.args.get('error') -%}
|
{%- if request.args.get('error') -%}
|
||||||
<div class="infobox plank critical">
|
{{infobox(request.args.error, InfoboxKind.ERROR)}}
|
||||||
{{request.args.get('error')}}
|
|
||||||
</div>
|
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<form class="plank primary-bg full-width" method="POST">
|
<form class="plank primary-bg full-width" method="POST">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
|
|||||||
30
app/templates/users/posts.html
Normal file
30
app/templates/users/posts.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{%- from 'common/macros.html' import full_post with context -%}
|
||||||
|
{%- from 'common/macros.html' import subheader, pager -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}{{ target_user.get_readable_name() }}'s posts{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
{%- set td -%}
|
||||||
|
<a href="{{url_for('users.user_page', username=target_user.username)}}">← Back to profile</a>
|
||||||
|
{%- endset -%}
|
||||||
|
{%- call() subheader("%s's posts" % target_user.get_readable_name(), td) -%}
|
||||||
|
{%- if posts -%}
|
||||||
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
|
<legend>Page</legend>
|
||||||
|
{{- pager(page, page_count) -}}
|
||||||
|
</fieldset>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endcall -%}
|
||||||
|
{%- if posts -%}
|
||||||
|
{%- for post in posts -%}
|
||||||
|
<div class="post plank">{{full_post(post, show_toolbar=false, show_thread=true, allow_reacting=false)}}</div>
|
||||||
|
{%- endfor -%}
|
||||||
|
<div class="plank">
|
||||||
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
|
<legend>Page</legend>
|
||||||
|
{{- pager(page, page_count) -}}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
{%- else -%}
|
||||||
|
<div class="plank">{{target_user.get_readable_name()}} has no posts.</div>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endblock -%}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% from 'common/macros.html' import infobox with context %}
|
||||||
{% from 'common/macros.html' import subheader %}
|
{% from 'common/macros.html' import subheader %}
|
||||||
{%- extends 'base.html' -%}
|
{%- extends 'base.html' -%}
|
||||||
{%- block title -%}sign up{%- endblock -%}
|
{%- block title -%}sign up{%- endblock -%}
|
||||||
@@ -7,9 +8,7 @@ Please read the rules etc. stub
|
|||||||
{%- endset -%}
|
{%- endset -%}
|
||||||
{{ subheader('Sign up', welcome)}}
|
{{ subheader('Sign up', welcome)}}
|
||||||
{%- if request.args.get('error') -%}
|
{%- if request.args.get('error') -%}
|
||||||
<div class="infobox plank critical">
|
{{infobox(request.args.error, InfoboxKind.ERROR)}}
|
||||||
{{request.args.get('error')}}
|
|
||||||
</div>
|
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<form class="plank primary-bg full-width" method="POST">
|
<form class="plank primary-bg full-width" method="POST">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
|
|||||||
33
app/templates/users/threads.html
Normal file
33
app/templates/users/threads.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{%- from 'common/macros.html' import full_post with context -%}
|
||||||
|
{%- from 'common/macros.html' import subheader, pager -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}threads by {{ target_user.get_readable_name() }}{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
{%- set td -%}
|
||||||
|
<a href="{{url_for('users.user_page', username=target_user.username)}}">← Back to profile</a>
|
||||||
|
{%- endset -%}
|
||||||
|
{%- call() subheader("%s's started threads" % target_user.get_readable_name(), td) -%}
|
||||||
|
{%- if threads -%}
|
||||||
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
|
<legend>Page</legend>
|
||||||
|
{{- pager(page, page_count) -}}
|
||||||
|
</fieldset>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endcall -%}
|
||||||
|
{%- if threads -%}
|
||||||
|
{%- for post in threads -%}
|
||||||
|
<div class="plank">
|
||||||
|
<h2 class="info"><a href="{{url_for('threads.thread_by_id', thread_id=post.thread_id)}}">"{{post.thread_title}}"</a> in topic <a href="{{url_for('topics.topic_by_id', topic_id=post.topic_id)}}">{{post.topic_name}}</a></h2>
|
||||||
|
<div class="post">{{full_post(post, show_toolbar=false, allow_reacting=false)}}</div>
|
||||||
|
</div>
|
||||||
|
{%- endfor -%}
|
||||||
|
<div class="plank">
|
||||||
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
|
<legend>Page</legend>
|
||||||
|
{{- pager(page, page_count) -}}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
{%- else -%}
|
||||||
|
<div class="plank">{{target_user.get_readable_name()}} has not started any threads.</div>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endblock -%}
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if get_active_user().is_mod() and target_user.id != get_active_user().id -%}
|
{%- if get_active_user().is_mod() and target_user.id != get_active_user().id and target_user.permission < get_active_user().permission -%}
|
||||||
<fieldset class="plank even no-shadow minimal thread-actions">
|
<fieldset class="plank even no-shadow minimal thread-actions">
|
||||||
<legend>Moderation actions</legend>
|
<legend>Moderation actions</legend>
|
||||||
<form method="POST">
|
<form class="thread-actions" method="POST">
|
||||||
{{csrf_input() | safe}}
|
{{csrf_input() | safe}}
|
||||||
{%- if target_user.is_guest() -%}
|
{%- if target_user.is_guest() -%}
|
||||||
<input class="warn" type="submit" value="Approve user" formaction="{{url_for('mod.make_user_regular', user_id=target_user.id)}}">
|
<input class="warn" type="submit" value="Approve user" formaction="{{url_for('mod.make_user_regular', user_id=target_user.id)}}">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from flask import url_for, session
|
from flask import url_for, session, request
|
||||||
from .models import Posts, Threads
|
from .models import Posts, Threads
|
||||||
from .auth import is_logged_in
|
from .auth import is_logged_in
|
||||||
|
import time
|
||||||
|
|
||||||
def get_post_url(post_id, _anchor=False, external=False):
|
def get_post_url(post_id, _anchor=False, external=False):
|
||||||
post = Posts.find({'id': post_id})
|
post = Posts.find({'id': post_id})
|
||||||
@@ -24,3 +25,9 @@ def get_csrf_token():
|
|||||||
|
|
||||||
def csrf_input():
|
def csrf_input():
|
||||||
return f'<input type="hidden" name="csrf" value="{get_csrf_token()}">'
|
return f'<input type="hidden" name="csrf" value="{get_csrf_token()}">'
|
||||||
|
|
||||||
|
def get_form_checkbox(name: str) -> bool:
|
||||||
|
return request.form.get(name, None) == 'on'
|
||||||
|
|
||||||
|
def time_now() -> int:
|
||||||
|
return int(time.time())
|
||||||
|
|||||||
@@ -103,8 +103,11 @@ button, .linkbutton, input[type="submit"] {
|
|||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
border: solid var(--border-thickness) var(--border-color);
|
border: solid var(--border-thickness) var(--border-color);
|
||||||
background: linear-gradient(var(--top-color) 0%, var(--top-color2) 25%, var(--main-color) 26%, var(--main-color) 50%, var(--bottom-color) 100%);
|
background: linear-gradient(var(--top-color) 0%, var(--top-color2) 25%, var(--main-color) 26%, var(--main-color) 50%, var(--bottom-color) 100%);
|
||||||
box-shadow: inset 0px 2px 5px 3px var(--inset-color);
|
/* box-shadow: inset 0px 2px 5px 3px var(--inset-color); */
|
||||||
color: var(--font-color);
|
/* color: var(--font-color); */
|
||||||
|
/* HACK: better than contrast-color on critical */
|
||||||
|
/* https://css-tricks.com/approximating-contrast-color-with-other-css-features/ */
|
||||||
|
color: oklch(from var(--main-color) round(1.21 - L) 0 0);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
@@ -179,7 +182,7 @@ button, .linkbutton, input[type="submit"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
|
min-height: 250px;
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -189,7 +192,7 @@ button, .linkbutton, input[type="submit"] {
|
|||||||
|
|
||||||
.babycode-editor {
|
.babycode-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 150px;
|
min-height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-edit-form {
|
.post-edit-form {
|
||||||
@@ -298,7 +301,7 @@ a.site-title {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
margin: 0;
|
margin: var(--base-padding) 0;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -405,14 +408,14 @@ ul.horizontal, ol.horizontal {
|
|||||||
.infobox {
|
.infobox {
|
||||||
--main-color: var(--infobox-color);
|
--main-color: var(--infobox-color);
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
|
color: oklch(from var(--main-color) round(1.21 - L) 0 0);
|
||||||
|
|
||||||
&.critical {
|
&.critical {
|
||||||
--main-color: hsl(from var(--critical-color) h 50% calc(l * 0.7));
|
--main-color: hsl(from var(--critical-color) h 50% l);
|
||||||
color: var(--font-color-anti);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warn {
|
&.warn {
|
||||||
--main-color: hsl(from var(--warn-color) h 50% calc(l * 1.2));
|
--main-color: hsl(from var(--warn-color) h 50% 75%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +444,7 @@ ul.horizontal, ol.horizontal {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--base-padding);
|
gap: var(--base-padding);
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
align-items: end;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,6 +620,11 @@ form.full-width {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-explain {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
/* babycode tags */
|
/* babycode tags */
|
||||||
.inline-code {
|
.inline-code {
|
||||||
background-color: var(--code-bg-color);
|
background-color: var(--code-bg-color);
|
||||||
@@ -676,6 +684,88 @@ pre code {
|
|||||||
color: var(--font-color-anti);
|
color: var(--font-color-anti);
|
||||||
padding: var(--base-padding);
|
padding: var(--base-padding);
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
.hll { background-color: #6e7681 }
|
||||||
|
.c { color: #8B949E; font-style: italic } /* Comment */
|
||||||
|
.err { color: #F85149 } /* Error */
|
||||||
|
.esc { color: #E6EDF3 } /* Escape */
|
||||||
|
.g { color: #E6EDF3 } /* Generic */
|
||||||
|
.k { color: #FF7B72 } /* Keyword */
|
||||||
|
.l { color: #A5D6FF } /* Literal */
|
||||||
|
.n { color: #E6EDF3 } /* Name */
|
||||||
|
.o { color: #FF7B72; font-weight: bold } /* Operator */
|
||||||
|
.x { color: #E6EDF3 } /* Other */
|
||||||
|
.p { color: #E6EDF3 } /* Punctuation */
|
||||||
|
.ch { color: #8B949E; font-style: italic } /* Comment.Hashbang */
|
||||||
|
.cm { color: #8B949E; font-style: italic } /* Comment.Multiline */
|
||||||
|
.cp { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Preproc */
|
||||||
|
.cpf { color: #8B949E; font-style: italic } /* Comment.PreprocFile */
|
||||||
|
.c1 { color: #8B949E; font-style: italic } /* Comment.Single */
|
||||||
|
.cs { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Special */
|
||||||
|
.gd { color: #FFA198; background-color: #490202 } /* Generic.Deleted */
|
||||||
|
.ge { color: #E6EDF3; font-style: italic } /* Generic.Emph */
|
||||||
|
.ges { color: #E6EDF3; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||||
|
.gr { color: #FFA198 } /* Generic.Error */
|
||||||
|
.gh { color: #79C0FF; font-weight: bold } /* Generic.Heading */
|
||||||
|
.gi { color: #56D364; background-color: #0F5323 } /* Generic.Inserted */
|
||||||
|
.go { color: #8B949E } /* Generic.Output */
|
||||||
|
.gp { color: #8B949E } /* Generic.Prompt */
|
||||||
|
.gs { color: #E6EDF3; font-weight: bold } /* Generic.Strong */
|
||||||
|
.gu { color: #79C0FF } /* Generic.Subheading */
|
||||||
|
.gt { color: #FF7B72 } /* Generic.Traceback */
|
||||||
|
.g-Underline { color: #E6EDF3; text-decoration: underline } /* Generic.Underline */
|
||||||
|
.kc { color: #79C0FF } /* Keyword.Constant */
|
||||||
|
.kd { color: #FF7B72 } /* Keyword.Declaration */
|
||||||
|
.kn { color: #FF7B72 } /* Keyword.Namespace */
|
||||||
|
.kp { color: #79C0FF } /* Keyword.Pseudo */
|
||||||
|
.kr { color: #FF7B72 } /* Keyword.Reserved */
|
||||||
|
.kt { color: #FF7B72 } /* Keyword.Type */
|
||||||
|
.ld { color: #79C0FF } /* Literal.Date */
|
||||||
|
.m { color: #A5D6FF } /* Literal.Number */
|
||||||
|
.s { color: #A5D6FF } /* Literal.String */
|
||||||
|
.na { color: #E6EDF3 } /* Name.Attribute */
|
||||||
|
.nb { color: #E6EDF3 } /* Name.Builtin */
|
||||||
|
.nc { color: #F0883E; font-weight: bold } /* Name.Class */
|
||||||
|
.no { color: #79C0FF; font-weight: bold } /* Name.Constant */
|
||||||
|
.nd { color: #D2A8FF; font-weight: bold } /* Name.Decorator */
|
||||||
|
.ni { color: #FFA657 } /* Name.Entity */
|
||||||
|
.ne { color: #F0883E; font-weight: bold } /* Name.Exception */
|
||||||
|
.nf { color: #D2A8FF; font-weight: bold } /* Name.Function */
|
||||||
|
.nl { color: #79C0FF; font-weight: bold } /* Name.Label */
|
||||||
|
.nn { color: #FF7B72 } /* Name.Namespace */
|
||||||
|
.nx { color: #E6EDF3 } /* Name.Other */
|
||||||
|
.py { color: #79C0FF } /* Name.Property */
|
||||||
|
.nt { color: #7EE787 } /* Name.Tag */
|
||||||
|
.nv { color: #79C0FF } /* Name.Variable */
|
||||||
|
.ow { color: #FF7B72; font-weight: bold } /* Operator.Word */
|
||||||
|
.pm { color: #E6EDF3 } /* Punctuation.Marker */
|
||||||
|
.w { color: #6E7681 } /* Text.Whitespace */
|
||||||
|
.mb { color: #A5D6FF } /* Literal.Number.Bin */
|
||||||
|
.mf { color: #A5D6FF } /* Literal.Number.Float */
|
||||||
|
.mh { color: #A5D6FF } /* Literal.Number.Hex */
|
||||||
|
.mi { color: #A5D6FF } /* Literal.Number.Integer */
|
||||||
|
.mo { color: #A5D6FF } /* Literal.Number.Oct */
|
||||||
|
.sa { color: #79C0FF } /* Literal.String.Affix */
|
||||||
|
.sb { color: #A5D6FF } /* Literal.String.Backtick */
|
||||||
|
.sc { color: #A5D6FF } /* Literal.String.Char */
|
||||||
|
.dl { color: #79C0FF } /* Literal.String.Delimiter */
|
||||||
|
.sd { color: #A5D6FF } /* Literal.String.Doc */
|
||||||
|
.s2 { color: #A5D6FF } /* Literal.String.Double */
|
||||||
|
.se { color: #79C0FF } /* Literal.String.Escape */
|
||||||
|
.sh { color: #79C0FF } /* Literal.String.Heredoc */
|
||||||
|
.si { color: #A5D6FF } /* Literal.String.Interpol */
|
||||||
|
.sx { color: #A5D6FF } /* Literal.String.Other */
|
||||||
|
.sr { color: #79C0FF } /* Literal.String.Regex */
|
||||||
|
.s1 { color: #A5D6FF } /* Literal.String.Single */
|
||||||
|
.ss { color: #A5D6FF } /* Literal.String.Symbol */
|
||||||
|
.bp { color: #E6EDF3 } /* Name.Builtin.Pseudo */
|
||||||
|
.fm { color: #D2A8FF; font-weight: bold } /* Name.Function.Magic */
|
||||||
|
.vc { color: #79C0FF } /* Name.Variable.Class */
|
||||||
|
.vg { color: #79C0FF } /* Name.Variable.Global */
|
||||||
|
.vi { color: #79C0FF } /* Name.Variable.Instance */
|
||||||
|
.vm { color: #79C0FF } /* Name.Variable.Magic */
|
||||||
|
.il { color: #A5D6FF } /* Literal.Number.Integer.Long */
|
||||||
}
|
}
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
|
|||||||
1
data/static/icons/bookmark.svg
Normal file
1
data/static/icons/bookmark.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="8" x2="8" y1="2" y2="14"><stop offset="0" stop-color="#cdcdcd"/><stop offset=".5" stop-color="#e6e6e6"/><stop offset="1" stop-color="#cdcdcd"/></linearGradient><g><path d="m3 1h10c.554 0 1 .446 1 1v9.5l-3.5 3.5h-7.5c-.554 0-1-.446-1-1v-12c0-.554.446-1 1-1z" fill="#515151" stroke-linecap="round" stroke-linejoin="round"/><path d="m3.5 2h9c.277 0 .5.223.5.5v8.5l-3 3h-6.5c-.277 0-.5-.223-.5-.5v-11c0-.277.223-.5.5-.5z" fill="url(#a)" stroke-linecap="round" stroke-linejoin="round"/><path d="m4 1v7.0000002l1-1.0000002 1 1.0000002v-7.0000002z" fill="#ff522f"/><path d="m10 13.5 2.5-2.5h-3c1 1 .5 2.5.5 2.5z" fill="#515151"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 816 B |
1
data/static/icons/error.svg
Normal file
1
data/static/icons/error.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(.921875 0 0 .9225 1.249998 1.239999)" gradientUnits="userSpaceOnUse" x1="16" x2="16" y1="0" y2="32"><stop offset=".15427744" stop-color="#de4747"/><stop offset="1" stop-color="#b52222"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="15.99999832" x2="15.99999832" y1=".126983" y2="31.87301564"><stop offset="0" stop-color="#f09797"/><stop offset=".73127526" stop-color="#d24141"/></linearGradient><filter id="c" color-interpolation-filters="sRGB" height="1.09976" width="1" x="0" y="0"><feFlood flood-opacity=".14902" in="SourceGraphic" result="flood"/><feGaussianBlur in="SourceGraphic" result="blur" stdDeviation="0"/><feOffset dx="0" dy="1.5" in="blur" result="offset"/><feComposite in="flood" in2="offset" operator="in" result="comp1"/><feComposite in="SourceGraphic" in2="comp1" operator="over" result="comp2"/></filter><g><circle cx="16" cy="16" fill="#590e0e" fill-opacity=".500866" r="16"/><circle cx="16" cy="16" fill="url(#b)" r="15.625" stroke="#7d4d4d" stroke-linecap="round" stroke-linejoin="round" stroke-width=".25"/><ellipse cx="16" cy="16" fill="url(#a)" rx="14.75" ry="14.76"/><g fill="#fff"><path d="m8 10 2-2 6 6 6-6 2 2-6 6 6 6-2 2-6-6-6 6-2-2 6-6z" filter="url(#c)"/><path d="m29.494141 10.042735a14.75 14.76 0 0 0 -13.494141-8.8027352 14.75 14.76 0 0 0 -13.49414 8.8027352c2.987994-2.3171382 8.240661-4.0429692 13.49414-4.0429692s10.506146 1.725831 13.494141 4.0429692z" fill-opacity=".216645" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
1
data/static/icons/info.svg
Normal file
1
data/static/icons/info.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><filter id="a" color-interpolation-filters="sRGB" height="1.09976" width="1" x="0" y="0"><feFlood flood-opacity=".14902" in="SourceGraphic" result="flood"/><feGaussianBlur in="SourceGraphic" result="blur" stdDeviation="0"/><feOffset dx="0" dy="1.5" in="blur" result="offset"/><feComposite in="flood" in2="offset" operator="in" result="comp1"/><feComposite in="SourceGraphic" in2="comp1" operator="over" result="comp2"/></filter><linearGradient id="b" gradientTransform="matrix(.921875 0 0 .9225 1.249998 1.239999)" gradientUnits="userSpaceOnUse" x1="16" x2="16" y1="0" y2="32"><stop offset=".15427744" stop-color="#4b8be1" stop-opacity=".992157"/><stop offset="1" stop-color="#1078e8" stop-opacity=".992157"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="15.99999832" x2="15.99999832" y1=".126983" y2="31.87301564"><stop offset="0" stop-color="#a9cef8"/><stop offset=".73127526" stop-color="#4f99e7"/></linearGradient><g><circle cx="16" cy="16" fill="#0e3459" fill-opacity=".500866" r="16"/><circle cx="16" cy="16" fill="url(#c)" r="15.625" stroke="#4d677d" stroke-linecap="round" stroke-linejoin="round" stroke-width=".25"/><ellipse cx="16" cy="16" fill="url(#b)" rx="14.75" ry="14.76"/><g fill="#fff" stroke-linecap="round" stroke-linejoin="round"><path d="m29.494141 10.042735a14.75 14.76 0 0 0 -13.494141-8.8027352 14.75 14.76 0 0 0 -13.49414 8.8027352c2.987994-2.3171382 8.240661-4.0429692 13.49414-4.0429692s10.506146 1.725831 13.494141 4.0429692z" fill-opacity=".216645"/><path d="m15.716477 10.189534q-.789603 0-1.344793-.5428521-.542852-.5551897-.542852-1.3447928 0-.7772656.542852-1.3201177.55519-.5551897 1.344793-.5551897.777266 0 1.320118.5551897.55519.5428521.55519 1.3201177 0 .7896031-.55519 1.3447928-.542852.5428521-1.320118.5428521zm1.529856 14.780383h-2.282446v-11.091456h-1.233755v-1.86297h3.516201z" filter="url(#a)" stroke-width=".25"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
1
data/static/icons/locked.svg
Normal file
1
data/static/icons/locked.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a"><stop offset="0" stop-color="#cc8e17" stop-opacity=".546079"/><stop offset=".34962881" stop-color="#cc7e17" stop-opacity=".202344"/><stop offset=".65078419" stop-color="#cc7e17" stop-opacity=".204745"/><stop offset="1" stop-color="#cc8e17" stop-opacity=".545098"/></linearGradient><linearGradient id="b"><stop offset="0" stop-color="#fff" stop-opacity=".748929"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient id="c"><stop offset="0" stop-color="#ecaa39"/><stop offset="1" stop-color="#ecaa39" stop-opacity="0"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="3" x2="13" xlink:href="#a" y1="12.5" y2="12.5"/><linearGradient id="e" gradientUnits="userSpaceOnUse" x1="11.5" x2="11.5" xlink:href="#b" y1="7" y2="14"/><linearGradient id="f" gradientUnits="userSpaceOnUse" x1="4.5" x2="4.5" xlink:href="#b" y1="7" y2="14"/><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="3.5" x2="3.5" xlink:href="#c" y1="14" y2="7"/><linearGradient id="h" gradientUnits="userSpaceOnUse" x1="12.5" x2="12.5" xlink:href="#c" y1="14" y2="7"/><radialGradient id="i" cx="8" cy="6.928571" gradientTransform="matrix(0 1.2000001 -.9999999 -.00000004 14.928571 -1.600001)" gradientUnits="userSpaceOnUse" r="5"><stop offset="0" stop-color="#ffeabe"/><stop offset="1" stop-color="#dab270"/></radialGradient><linearGradient id="j" gradientUnits="userSpaceOnUse" x1="3" x2="13" xlink:href="#a" y1="10.5" y2="10.5"/><linearGradient id="k" gradientUnits="userSpaceOnUse" x1="3" x2="13" xlink:href="#a" y1="8.5" y2="8.5"/><linearGradient id="l" gradientUnits="userSpaceOnUse" x1="8" x2="8" y1="7" y2="7.5"><stop offset="0" stop-color="#eee" stop-opacity=".75476"/><stop offset="1" stop-color="#eee" stop-opacity="0"/></linearGradient><rect fill="#876c3d" height="9" ry="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-width=".250001" width="12" x="2" y="6"/><rect fill="url(#i)" height="7" ry=".5" stroke-linecap="round" stroke-linejoin="round" stroke-width=".25" width="10" x="3" y="7"/><path d="m8 1c-4 0-4 4-4 4v1h1v-1s0-3 3-3 3 3 3 3v1h1v-1s0-4-4-4zm0 2c-2 0-2 2-2 2v1h1v-1s0-1 1-1 1 1 1 1v1h1v-1s0-2-2-2z" fill="#515151"/><path d="m5 6v-1s0-3 3-3 3 3 3 3v1h-1v-1s0-2-2-2-2 2-2 2v1z" fill="#eee"/><path d="m3 12h10v1h-10z" fill="url(#d)"/><rect fill="url(#e)" height="7" rx=".5" ry="1" width="1" x="11" y="7"/><rect fill="url(#f)" height="7" rx=".5" ry="1" width="1" x="4" y="7"/><rect fill="url(#g)" height="7" rx=".5" width="1" x="3" y="7"/><rect fill="url(#h)" height="7" rx=".5" width="1" x="12" y="7"/><path d="m3 10h10v1h-10z" fill="url(#j)"/><path d="m3 8h10v1h-10z" fill="url(#k)"/><rect fill="url(#l)" height="1" rx=".5" width="10" x="3" y="7"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
1
data/static/icons/sticky.svg
Normal file
1
data/static/icons/sticky.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a"><stop offset="0" stop-color="#9a2424"/><stop offset="1" stop-color="#c82e2e"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="8" x2="8" y1="2" y2="14"><stop offset="0" stop-color="#f0dd85"/><stop offset=".5" stop-color="#f7ecaf"/><stop offset="1" stop-color="#f0dd85"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="8" x2="8" xlink:href="#a" y1="5.5" y2="9.5"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="8" x2="8" xlink:href="#a" y1="5.5" y2="4"/><linearGradient id="e" gradientUnits="userSpaceOnUse" x1="8" x2="8" xlink:href="#a" y1="10.5" y2="6.145898"/><g><path d="m3 2h10c.554 0 1 .446 1 1v7.5l-3.5 3.5h-7.5c-.554 0-1-.446-1-1v-10c0-.554.446-1 1-1z" fill="#6e632e" stroke-linecap="round" stroke-linejoin="round"/><path d="m3.5 3h9c.277 0 .5.223.5.5v6.5l-3 3h-6.5c-.277 0-.5-.223-.5-.5v-9c0-.277.223-.5.5-.5z" fill="url(#b)" stroke-linecap="round" stroke-linejoin="round"/><path d="m10 12.5 2.5-2.5h-3c1 1 .5 2.5.5 2.5z" fill="#6e632e"/><ellipse cx="6.5" cy="9.50974" fill-opacity=".355175" rx="2.5" ry="1.49026"/><g transform="matrix(.70710678 .70710678 -.70710678 .70710678 11.449748 -6.763456)"><path d="m7.5 10.5v2.5l.5 1 .5-1v-2.5z" fill="#cfcfcf"/><path d="m7 10.5h.5v2.5l.5 1 .5-1v-2.5h.5v2.5l-1 2-1-2z" fill="#727272"/><ellipse cx="8" cy="9.5" fill="url(#e)" rx="3" ry="1.5"/><path d="m6 9 .5-4h3l.5 4c0 .5-1 1-2 1s-2-.5-2-1z" fill="url(#c)"/><ellipse cx="8" cy="5" fill="url(#d)" rx="2" ry="1"/><g fill="#d23d3d"><path d="m6 5s.5.5 2 .5 2-.5 2-.5c0 1-2 1-2 1s-2 0-2-1z"/><path d="m5 9.5s.5 1 3 1 3-1 3-1c0 1.5-3 1.5-3 1.5s-3 0-3-1.5z"/></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
1
data/static/icons/warn.svg
Normal file
1
data/static/icons/warn.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><filter id="a" color-interpolation-filters="sRGB" height="1.09976" width="1" x="0" y="0"><feFlood flood-opacity=".14902" in="SourceGraphic" result="flood"/><feGaussianBlur in="SourceGraphic" result="blur" stdDeviation="0"/><feOffset dx="0" dy="1.5" in="blur" result="offset"/><feComposite in="flood" in2="offset" operator="in" result="comp1"/><feComposite in="SourceGraphic" in2="comp1" operator="over" result="comp2"/></filter><linearGradient id="b" gradientTransform="matrix(1.01415 0 0 .99686492 -.226398 -.52981)" gradientUnits="userSpaceOnUse" x1="16" x2="15.999999" y1="1.302475" y2="30.000002"><stop offset="0" stop-color="#f4f4ed"/><stop offset=".72657222" stop-color="#ddcf76"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="16" x2="16" y1="2.369791" y2="29.630209"><stop offset=".25" stop-color="#edd86d"/><stop offset="1" stop-color="#d5901a"/></linearGradient><g><path d="m14.960961 2.718822-14.675898 25.36219a1.2153386 1.2001658 0 0 0 1.055209 1.795616h29.319455a1.2153386 1.2001658 0 0 0 1.05521-1.795616l-14.6759-25.36219a1.1967125 1.1817724 0 0 0 -2.078076 0z" fill="#443c09" fill-opacity=".500632"/><path d="m14.99368 3.1949619-14.21375986 24.4501341a1.1770681 1.1570063 0 0 0 1.02198136 1.731044h28.3961965a1.1770681 1.1570063 0 0 0 1.021982-1.731044l-14.213761-24.4501341a1.1590285 1.1392744 0 0 0 -2.012639 0z" fill="url(#b)" stroke="#6b5b07" stroke-linecap="round" stroke-linejoin="round" stroke-width=".25"/><path d="m15.11254 4.0052032-12.6044459 22.9058128c-.3954388.72251.1042399 1.621487.901271 1.621487l25.1812709-.0031c.79703-.000097 1.296709-.898977.90127-1.621487l-12.604448-22.9027128c-.39239-.7169421-1.382527-.7169421-1.774918 0z" fill="url(#c)"/><g stroke-linecap="round" stroke-linejoin="round"><path d="m11.150645 10.205078c1.849355-1.205078 3.775283-1.205078 4.848141-1.205078 1.072971 0 3.001214 0 4.851104 1.205078l-3.962432-6.1998748c-.39239-.716943-1.383-.716943-1.775391 0z" fill="#fff" fill-opacity=".397479"/><path d="m16.00535 25.361656q-.620708 0-1.059484-.428074-.428074-.438776-.428074-1.059484 0-.610006.428074-1.03808.438776-.438776 1.059484-.438776.610005 0 1.03808.438776.438776.428074.438776 1.03808 0 .620708-.438776 1.059484-.428075.428074-1.03808.428074zm.791937-4.494779h-1.551769l-.310354-10.541328h2.129669z" fill="#443c09" filter="url(#a)" stroke-width=".220327" transform="matrix(1.1346739 0 0 1.1346739 -2.154783 -2.497989)"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
Reference in New Issue
Block a user