Compare commits
7 Commits
3da3054587
...
56c531b64e
Author | SHA1 | Date | |
---|---|---|---|
56c531b64e | |||
d729924101 | |||
58dd9fb439 | |||
7ef0b9dc7d | |||
4f18694de3 | |||
285c1cb119 | |||
76da1c3e61 |
@ -4,7 +4,7 @@ from .models import Avatars, Users
|
|||||||
from .auth import digest
|
from .auth import digest
|
||||||
from .routes.users import is_logged_in, get_active_user
|
from .routes.users import is_logged_in, get_active_user
|
||||||
from .constants import (
|
from .constants import (
|
||||||
PermissionLevel,
|
PermissionLevel, permission_level_string,
|
||||||
InfoboxKind, InfoboxIcons, InfoboxHTMLClass
|
InfoboxKind, InfoboxIcons, InfoboxHTMLClass
|
||||||
)
|
)
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -77,12 +77,14 @@ def create_app():
|
|||||||
from app.routes.users import bp as users_bp
|
from app.routes.users import bp as users_bp
|
||||||
from app.routes.mod import bp as mod_bp
|
from app.routes.mod import bp as mod_bp
|
||||||
from app.routes.api import bp as api_bp
|
from app.routes.api import bp as api_bp
|
||||||
|
from app.routes.posts import bp as posts_bp
|
||||||
app.register_blueprint(app_bp)
|
app.register_blueprint(app_bp)
|
||||||
app.register_blueprint(topics_bp)
|
app.register_blueprint(topics_bp)
|
||||||
app.register_blueprint(threads_bp)
|
app.register_blueprint(threads_bp)
|
||||||
app.register_blueprint(users_bp)
|
app.register_blueprint(users_bp)
|
||||||
app.register_blueprint(mod_bp)
|
app.register_blueprint(mod_bp)
|
||||||
app.register_blueprint(api_bp)
|
app.register_blueprint(api_bp)
|
||||||
|
app.register_blueprint(posts_bp)
|
||||||
|
|
||||||
app.config['SESSION_COOKIE_SECURE'] = True
|
app.config['SESSION_COOKIE_SECURE'] = True
|
||||||
|
|
||||||
@ -118,4 +120,8 @@ def create_app():
|
|||||||
|
|
||||||
return plural
|
return plural
|
||||||
|
|
||||||
|
@app.template_filter("permission_string")
|
||||||
|
def permission_string(term):
|
||||||
|
return permission_level_string(term)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
@ -7,6 +7,17 @@ class PermissionLevel(Enum):
|
|||||||
SYSTEM = 3
|
SYSTEM = 3
|
||||||
ADMIN = 4
|
ADMIN = 4
|
||||||
|
|
||||||
|
PermissionLevelString = {
|
||||||
|
PermissionLevel.GUEST: 'Guest',
|
||||||
|
PermissionLevel.USER: 'User',
|
||||||
|
PermissionLevel.MODERATOR: 'Moderator',
|
||||||
|
PermissionLevel.SYSTEM: 'System',
|
||||||
|
PermissionLevel.ADMIN: 'Administrator',
|
||||||
|
}
|
||||||
|
|
||||||
|
def permission_level_string(perm):
|
||||||
|
return PermissionLevelString[PermissionLevel(int(perm))]
|
||||||
|
|
||||||
class InfoboxKind(IntEnum):
|
class InfoboxKind(IntEnum):
|
||||||
INFO = 0
|
INFO = 0
|
||||||
LOCK = 1
|
LOCK = 1
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
from .db import db
|
from .db import db
|
||||||
|
|
||||||
# format: {integer: str|list<str>}
|
def migrate_old_avatars():
|
||||||
MIGRATIONS = {
|
for avatar in db.query('SELECT id, file_path FROM avatars WHERE file_path LIKE "/avatars/%"'):
|
||||||
|
new_path = f"/static{avatar['file_path']}"
|
||||||
|
db.execute('UPDATE avatars SET file_path = ? WHERE id = ?', new_path, avatar['id'])
|
||||||
|
|
||||||
}
|
# format: [str|tuple(str, any...)|callable]
|
||||||
|
MIGRATIONS = [
|
||||||
|
migrate_old_avatars,
|
||||||
|
]
|
||||||
|
|
||||||
def run_migrations():
|
def run_migrations():
|
||||||
db.execute("""
|
db.execute("""
|
||||||
@ -16,18 +21,21 @@ def run_migrations():
|
|||||||
return
|
return
|
||||||
print("Running migrations...")
|
print("Running migrations...")
|
||||||
ran = 0
|
ran = 0
|
||||||
completed = [row["id"] for row in db.query("SELECT id FROM _migrations")]
|
completed = {int(row["id"]) for row in db.query("SELECT id FROM _migrations")}
|
||||||
for migration_id in sorted(MIGRATIONS.keys()):
|
to_run = {idx: migration_obj for idx, migration_obj in enumerate(MIGRATIONS) if idx not in completed}
|
||||||
if migration_id not in completed:
|
if not to_run:
|
||||||
print(f"Running migration #{migration_id}")
|
print('No migrations need to run.')
|
||||||
|
return
|
||||||
|
|
||||||
|
with db.transaction():
|
||||||
|
for migration_id, migration_obj in to_run.items():
|
||||||
|
if isinstance(migration_obj, str):
|
||||||
|
db.execute(migration_obj)
|
||||||
|
elif isinstance(migration_obj, tuple):
|
||||||
|
db.execute(migration_obj[0], *migration_obj[1:])
|
||||||
|
elif callable(migration_obj):
|
||||||
|
migration_obj()
|
||||||
|
|
||||||
|
db.execute('INSERT INTO _migrations (id) VALUES (?)', migration_id)
|
||||||
ran += 1
|
ran += 1
|
||||||
statements = MIGRATIONS[migration_id]
|
|
||||||
# support both strings and lists
|
|
||||||
if isinstance(statements, str):
|
|
||||||
statements = [statements]
|
|
||||||
|
|
||||||
for sql in statements:
|
|
||||||
db.execute(sql)
|
|
||||||
|
|
||||||
db.execute("INSERT INTO _migrations (id) VALUES (?)", migration_id)
|
|
||||||
print(f"Ran {ran} migrations.")
|
print(f"Ran {ran} migrations.")
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
from flask import Blueprint, redirect, url_for
|
from flask import (
|
||||||
|
Blueprint, redirect, url_for, flash, render_template, request
|
||||||
|
)
|
||||||
|
from .users import login_required, get_active_user
|
||||||
from ..lib.babycode import babycode_to_html
|
from ..lib.babycode import babycode_to_html
|
||||||
|
from ..constants import InfoboxKind
|
||||||
from ..db import db
|
from ..db import db
|
||||||
from ..models import Posts, PostHistory
|
from ..models import Posts, PostHistory, Threads
|
||||||
|
|
||||||
|
bp = Blueprint("posts", __name__, url_prefix = "/post")
|
||||||
|
|
||||||
bp = Blueprint("posts", __name__, url_prefix = "/posts")
|
|
||||||
|
|
||||||
def create_post(thread_id, user_id, content, markup_language="babycode"):
|
def create_post(thread_id, user_id, content, markup_language="babycode"):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
@ -24,3 +29,71 @@ def create_post(thread_id, user_id, content, markup_language="babycode"):
|
|||||||
post.update({"current_revision_id": revision.id})
|
post.update({"current_revision_id": revision.id})
|
||||||
return post
|
return post
|
||||||
|
|
||||||
|
|
||||||
|
def update_post(post_id, new_content, markup_language='babycode'):
|
||||||
|
with db.transaction():
|
||||||
|
post = Posts.find({'id': post_id})
|
||||||
|
new_revision = PostHistory.create({
|
||||||
|
'post_id': post.id,
|
||||||
|
'content': babycode_to_html(new_content),
|
||||||
|
'is_initial_revision': False,
|
||||||
|
'original_markup': new_content,
|
||||||
|
'markup_language': markup_language,
|
||||||
|
})
|
||||||
|
|
||||||
|
post.update({'current_revision_id': new_revision.id})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/<post_id>/delete")
|
||||||
|
@login_required
|
||||||
|
def delete(post_id):
|
||||||
|
post = Posts.find({'id': post_id})
|
||||||
|
thread = Threads.find({'id': post.thread_id})
|
||||||
|
user = get_active_user()
|
||||||
|
|
||||||
|
if user.is_mod() or post.user_id == user.id:
|
||||||
|
post.delete()
|
||||||
|
flash("Post deleted.", InfoboxKind.INFO)
|
||||||
|
|
||||||
|
return redirect(url_for('threads.thread', slug=thread.slug))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/<post_id>/edit")
|
||||||
|
@login_required
|
||||||
|
def edit(post_id):
|
||||||
|
user = get_active_user()
|
||||||
|
q = f"{Posts.FULL_POSTS_QUERY} WHERE posts.id = ?"
|
||||||
|
editing_post = db.fetch_one(q, post_id)
|
||||||
|
if not editing_post:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
if editing_post['user_id'] != user.id:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
|
thread = Threads.find({'id': editing_post['thread_id']})
|
||||||
|
|
||||||
|
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'
|
||||||
|
prev_context = db.query(context_prev_q, thread.id, editing_post['created_at'])
|
||||||
|
next_context = db.query(context_next_q, thread.id, editing_post['created_at'])
|
||||||
|
|
||||||
|
return render_template('posts/edit.html',
|
||||||
|
editing_post = editing_post,
|
||||||
|
thread = thread,
|
||||||
|
prev_context = prev_context,
|
||||||
|
next_context = next_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/<post_id>/edit")
|
||||||
|
@login_required
|
||||||
|
def edit_form(post_id):
|
||||||
|
user = get_active_user()
|
||||||
|
post = Posts.find({'id': post_id})
|
||||||
|
if post.user_id != user.id:
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
|
update_post(post.id, request.form.get('new_content', default=''))
|
||||||
|
thread = Threads.find({'id': post.thread_id})
|
||||||
|
return redirect(url_for('threads.thread', slug=thread.slug, after=post.id, _anchor=f'post-{post.id}'))
|
||||||
|
@ -46,8 +46,9 @@ def thread(slug):
|
|||||||
'user_id': get_active_user().id,
|
'user_id': get_active_user().id,
|
||||||
})
|
})
|
||||||
if subscription:
|
if subscription:
|
||||||
|
if int(posts[-1]['created_at']) > int(subscription.last_seen):
|
||||||
subscription.update({
|
subscription.update({
|
||||||
'last_seen': int(time.time())
|
'last_seen': int(posts[-1]['created_at'])
|
||||||
})
|
})
|
||||||
is_subscribed = True
|
is_subscribed = True
|
||||||
|
|
||||||
@ -78,6 +79,13 @@ def reply(slug):
|
|||||||
post_content = request.form['post_content']
|
post_content = request.form['post_content']
|
||||||
post = create_post(thread.id, user.id, post_content)
|
post = create_post(thread.id, user.id, post_content)
|
||||||
|
|
||||||
|
subscription = Subscriptions.find({'user_id': user.id, 'thread_id': thread.id})
|
||||||
|
|
||||||
|
if subscription:
|
||||||
|
subscription.update({'last_seen': int(time.time())})
|
||||||
|
elif request.form.get('subscribe', default=None) == 'on':
|
||||||
|
Subscriptions.create({'user_id': user.id, 'thread_id': thread.id, 'last_seen': int(time.time())})
|
||||||
|
|
||||||
return redirect(url_for(".thread", slug=slug, after=post.id, _anchor="latest-post"))
|
return redirect(url_for(".thread", slug=slug, after=post.id, _anchor="latest-post"))
|
||||||
|
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ def settings_form(username):
|
|||||||
status = request.form.get('status', default="")[:100]
|
status = request.form.get('status', default="")[:100]
|
||||||
original_sig = request.form.get('signature', default='')
|
original_sig = request.form.get('signature', default='')
|
||||||
rendered_sig = babycode_to_html(original_sig)
|
rendered_sig = babycode_to_html(original_sig)
|
||||||
session['subscribe_by_default'] = request.form.get('subscribe_by_default', default='on') == 'on'
|
session['subscribe_by_default'] = request.form.get('subscribe_by_default', default='off') == 'on'
|
||||||
|
|
||||||
user.update({
|
user.update({
|
||||||
'status': status,
|
'status': status,
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
{% set show_edit = (active_user.id | string) == (post['user_id'] | string) and (not post['thread_is_locked'] or active_user.is_mod()) and not no_reply %}
|
{% set show_edit = (active_user.id | string) == (post['user_id'] | string) and (not post['thread_is_locked'] or active_user.is_mod()) and not no_reply %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if show_edit %}
|
{% if show_edit %}
|
||||||
<a class="linkbutton" href="#TODO">Edit</a>
|
<a class="linkbutton" href="{{ url_for('posts.edit', post_id=post.id, _anchor='babycode-content') }}">Edit</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set show_reply = true %}
|
{% set show_reply = true %}
|
||||||
|
18
app/templates/posts/edit.html
Normal file
18
app/templates/posts/edit.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% from 'common/macros.html' import full_post %}
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}editing a post{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% for post in prev_context | reverse %}
|
||||||
|
{{ full_post(post=post, no_reply=true, active_user=active_user) }}
|
||||||
|
{% endfor %}
|
||||||
|
<span class="context-explain">
|
||||||
|
<span>↑↑↑</span><i>Context</i><span>↑↑↑</span>
|
||||||
|
</span>
|
||||||
|
{{ full_post(post=editing_post, editing=true, no_reply=true, active_user=active_user) }}
|
||||||
|
<span class="context-explain">
|
||||||
|
<span>↓↓↓</span><i>Context</i><span>↓↓↓</span>
|
||||||
|
</span>
|
||||||
|
{% for post in next_context %}
|
||||||
|
{{ full_post(post=post, no_reply=true, active_user=active_user) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
@ -24,7 +24,7 @@
|
|||||||
<input type='text' id='status' name='status' value='{{ active_user.status }}' maxlength=100 placeholder='Will be shown under your name. Max 100 characters.'>
|
<input type='text' id='status' name='status' value='{{ active_user.status }}' maxlength=100 placeholder='Will be shown under your name. Max 100 characters.'>
|
||||||
<label for='babycode-content'>Signature</label>
|
<label for='babycode-content'>Signature</label>
|
||||||
{{ babycode_editor_component(ta_name='signature', prefill=active_user.signature_original_markup, ta_placeholder='Will be shown under each of your posts', optional=true) }}
|
{{ babycode_editor_component(ta_name='signature', prefill=active_user.signature_original_markup, ta_placeholder='Will be shown under each of your posts', optional=true) }}
|
||||||
<input autocomplete='off' type='checkbox' id='subscribe_by_default' {{ 'checked' if session.get('subscribe_by_default', default=true) else '' }}>
|
<input autocomplete='off' type='checkbox' id='subscribe_by_default' name='subscribe_by_default' {{ 'checked' if session.get('subscribe_by_default', default=true) else '' }}>
|
||||||
<label for='subscribe_by_default'>Subscribe to thread by default when responding</label><br>
|
<label for='subscribe_by_default'>Subscribe to thread by default when responding</label><br>
|
||||||
<input type='submit' value='Save settings'>
|
<input type='submit' value='Save settings'>
|
||||||
</form>
|
</form>
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
<div class="user-page-stats">
|
<div class="user-page-stats">
|
||||||
{% with stats = target_user.get_post_stats() %}
|
{% with stats = target_user.get_post_stats() %}
|
||||||
<ul class="user-stats-list">
|
<ul class="user-stats-list">
|
||||||
<li>Permission: {{ target_user.permission }}</li>
|
<li>Permission: {{ target_user.permission | permission_string }}</li>
|
||||||
<li>Posts created: {{ stats.post_count }}</li>
|
<li>Posts created: {{ stats.post_count }}</li>
|
||||||
<li>Threads started: {{ stats.thread_count }}</li>
|
<li>Threads started: {{ stats.thread_count }}</li>
|
||||||
{% if stats.latest_thread_title %}
|
{% if stats.latest_thread_title %}
|
||||||
|
@ -463,11 +463,11 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.contain-svg:not(.full) > svg, .contain-svg img {
|
.contain-svg:not(.full) > svg, .contain-svg:not(.full) > img {
|
||||||
height: 50%;
|
height: 50%;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
.contain-svg.full > svg, .contain-svg img {
|
.contain-svg.full > svg, .contain-svg.full > img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -552,7 +552,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
|
|
||||||
.topic {
|
.topic {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.5fr 64px;
|
grid-template-columns: 1.5fr 96px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0px 0px;
|
gap: 0px 0px;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
|
@ -472,11 +472,11 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
&:not(.full) > svg, img {
|
&:not(.full) > svg, &:not(.full) > img {
|
||||||
height: 50%;
|
height: 50%;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
&.full > svg, img {
|
&.full > svg, &.full > img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -564,7 +564,7 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
|
|
||||||
.topic {
|
.topic {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.5fr 64px;
|
grid-template-columns: 1.5fr 96px;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
gap: 0px 0px;
|
gap: 0px 0px;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
|
Loading…
Reference in New Issue
Block a user