add subscribing and unsubscribing, add post editing
This commit is contained in:
@@ -102,6 +102,9 @@ class Users(Model):
|
||||
def get_badges(self):
|
||||
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):
|
||||
table = 'topics'
|
||||
@@ -327,7 +330,8 @@ class Posts(Model):
|
||||
for mention in html_content.mentions:
|
||||
Mentions.create({
|
||||
'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'],
|
||||
'end_index': mention['end'],
|
||||
})
|
||||
@@ -335,6 +339,32 @@ class Posts(Model):
|
||||
post.update({'current_revision_id': revision.id})
|
||||
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):
|
||||
table = 'post_history'
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from flask import Blueprint, abort
|
||||
from flask import Blueprint, abort, render_template, redirect, url_for, request
|
||||
from functools import wraps
|
||||
from ..auth import login_required, get_active_user
|
||||
from ..models import Posts
|
||||
from ..models import Posts, Threads
|
||||
from ..util import get_post_url
|
||||
from ..db import db
|
||||
|
||||
bp = Blueprint('posts', __name__, url_prefix='/posts/')
|
||||
|
||||
@@ -9,10 +11,11 @@ def ownership_required(view_func):
|
||||
@wraps(view_func)
|
||||
def wrapper(*args, **kwargs):
|
||||
post = Posts.find({'id': kwargs.get('post_id', None)})
|
||||
user = get_active_user()
|
||||
if not post:
|
||||
abort(404)
|
||||
|
||||
if post.user_id != get_active_user().id:
|
||||
if post.user_id != user.id:
|
||||
abort(403)
|
||||
|
||||
return view_func(*args, **kwargs)
|
||||
@@ -35,7 +38,46 @@ def ownership_or_mod_required(view_func):
|
||||
@login_required
|
||||
@ownership_required
|
||||
def edit(post_id):
|
||||
return 'stub'
|
||||
post = Posts.find({'id': post_id})
|
||||
thread = Threads.find({'id': post.thread_id})
|
||||
user = get_active_user()
|
||||
if not thread:
|
||||
# what?
|
||||
abort(404)
|
||||
|
||||
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})
|
||||
thread = Threads.find({'id': post.thread_id})
|
||||
user = get_active_user()
|
||||
if not thread:
|
||||
# what?
|
||||
abort(404)
|
||||
|
||||
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/')
|
||||
@login_required
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from flask import Blueprint, redirect, url_for, render_template, request, abort
|
||||
from functools import wraps
|
||||
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
|
||||
|
||||
bp = Blueprint('threads', __name__, url_prefix='/threads/')
|
||||
@@ -56,7 +57,15 @@ def thread(thread_id, slug):
|
||||
page = max(1, min(int(request.args.get('page', default=1)), page_count))
|
||||
except ValueError:
|
||||
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>/')
|
||||
@login_required
|
||||
@@ -68,6 +77,13 @@ def reply(thread_id):
|
||||
if not user.can_post_to_thread_or_topic(thread):
|
||||
return redirect(url_for('.thread_by_id', thread_id=thread_id))
|
||||
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}'))
|
||||
|
||||
@bp.get('/<int:thread_id>/edit/')
|
||||
@@ -82,6 +98,48 @@ def edit(thread_id):
|
||||
def edit_post(thread_id):
|
||||
return 'stub'
|
||||
|
||||
@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/')
|
||||
def feed(thread_id):
|
||||
return 'stub'
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{%- from 'common/icons.html' import icn_info, icn_warn, icn_error, icn_bookmark -%}
|
||||
|
||||
{% 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>
|
||||
{#<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 %}
|
||||
|
||||
{% macro subheader(title, desc='') -%}
|
||||
@@ -103,9 +104,9 @@
|
||||
<button type="button" class="minimal">•</button>
|
||||
<button type="button" class="minimal"><img src="/static/emoji/angry.png" class="emoji"></button>
|
||||
</span>
|
||||
<a href="##">babycode help</a>
|
||||
</span>
|
||||
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}}>{{ prefill }}</textarea>
|
||||
<a href="##">babycode help</a>
|
||||
{%- endif -%}
|
||||
{%- endcall -%}
|
||||
{%- endmacro %}
|
||||
@@ -142,11 +143,17 @@
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<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>
|
||||
<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_toolbar -%}
|
||||
<span class="thread-actions">
|
||||
{%- 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 -%}
|
||||
{%- if can_reply -%}
|
||||
<button disabled title="This feature requires JavaScript to be enabled.">Quote</button>
|
||||
@@ -158,13 +165,18 @@
|
||||
</span>
|
||||
{%- endif -%}
|
||||
</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 not is_editing -%}
|
||||
{{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 -%}
|
||||
</div>
|
||||
{%- if show_reactions -%}
|
||||
<div class="plank even secondary-bg minimal no-shadow">
|
||||
{%- if show_reactions -%}
|
||||
<span class="button-row">
|
||||
{%- for reaction in Reactions.for_post(post.id) -%}
|
||||
{% set reactors = Reactions.get_users(post.id, reaction.reaction_text) | map(attribute='username') | list %}
|
||||
@@ -178,9 +190,13 @@
|
||||
{%- endfor -%}
|
||||
</span>
|
||||
{%- if is_logged_in() -%}<button disabled title="This feature requires JavaScript to be enabled.">Add reaction</button>{%- endif -%}
|
||||
</div>
|
||||
{%- 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>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro infobox(message, kind=InfoboxKind.INFO) -%}
|
||||
|
||||
24
app/templates/posts/edit.html
Normal file
24
app/templates/posts/edit.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{%- from 'common/macros.html' import full_post with context -%}
|
||||
{%- extends 'base.html' -%}
|
||||
{%- block title -%}editing a post{%- endblock -%}
|
||||
{%- block content -%}
|
||||
{%- 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 -%}
|
||||
@@ -24,7 +24,11 @@
|
||||
{%- if thread.user_id == get_active_user().id -%}
|
||||
<a class="linkbutton" href="{{url_for('threads.edit', thread_id=thread.id)}}">Edit</a>
|
||||
{%- endif -%}
|
||||
<button>Subscribe</button>
|
||||
<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 -%}
|
||||
<a href="{{url_for('threads.feed', thread_id=thread.id)}}" class="linkbutton rss">Subscribe via RSS</a>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from flask import url_for, session
|
||||
from flask import url_for, session, request
|
||||
from .models import Posts, Threads
|
||||
from .auth import is_logged_in
|
||||
import time
|
||||
|
||||
def get_post_url(post_id, _anchor=False, external=False):
|
||||
post = Posts.find({'id': post_id})
|
||||
@@ -24,3 +25,9 @@ def get_csrf_token():
|
||||
|
||||
def csrf_input():
|
||||
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())
|
||||
|
||||
@@ -182,7 +182,7 @@ button, .linkbutton, input[type="submit"] {
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
|
||||
min-height: 250px;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
@@ -192,7 +192,7 @@ button, .linkbutton, input[type="submit"] {
|
||||
|
||||
.babycode-editor {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.post-edit-form {
|
||||
@@ -620,6 +620,11 @@ form.full-width {
|
||||
}
|
||||
}
|
||||
|
||||
.context-explain {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
/* babycode tags */
|
||||
.inline-code {
|
||||
background-color: var(--code-bg-color);
|
||||
|
||||
Reference in New Issue
Block a user