Merge pull request #9 from Demindiro/mod-hide-threads

This commit is contained in:
David Hoppenbrouwers
2022-10-14 20:11:53 +02:00
committed by GitHub
11 changed files with 284 additions and 36 deletions

View File

@@ -32,6 +32,10 @@ Lastly, run with:
You will need a proxy such as nginx to access the forum on the public internet. You will need a proxy such as nginx to access the forum on the public internet.
## Upgrading
To upgrade from a previous version, run ``upgrade_sqlite.sh``
## Screenshots ## Screenshots
![Index](https://static.agreper.com/index.png) ![Index](https://static.agreper.com/index.png)

View File

@@ -19,7 +19,7 @@ class DB:
on t.thread_id = ( on t.thread_id = (
select tt.thread_id select tt.thread_id
from threads tt from threads tt
where f.forum_id = tt.forum_id where f.forum_id = tt.forum_id and not tt.hidden
order by update_time desc order by update_time desc
limit 1 limit 1
) )
@@ -35,31 +35,64 @@ class DB:
(forum_id,) (forum_id,)
).fetchone() ).fetchone()
def get_threads(self, forum_id, offset, limit): def get_threads(self, forum_id, offset, limit, user_id):
return self._db().execute(''' return self._db().execute('''
select t.thread_id, title, t.create_time, t.update_time, t.author_id, name, count(c.thread_id) select
from threads t, users t.thread_id,
left join comments c on t.thread_id = c.thread_id title,
where forum_id = ? and user_id = t.author_id t.create_time,
t.update_time,
t.author_id,
name,
count(c.thread_id),
t.hidden
from
threads t,
users
left join
comments c
on
t.thread_id = c.thread_id
where forum_id = ?
and user_id = t.author_id
and (
t.hidden = 0 or (
select 1 from users
where user_id = ?
and (
user_id = t.author_id
-- 1 = moderator, 2 = admin
or role in (1, 2)
)
)
)
group by t.thread_id group by t.thread_id
order by t.update_time desc order by t.update_time desc
limit ? limit ?
offset ? offset ?
''', ''',
(forum_id, limit, offset) (forum_id, user_id, limit, offset)
) )
def get_thread(self, thread): def get_thread(self, thread):
db = self._db() db = self._db()
title, text, author, author_id, create_time, modify_time = db.execute(''' title, text, author, author_id, create_time, modify_time, hidden = db.execute('''
select title, text, name, author_id, create_time, modify_time select title, text, name, author_id, create_time, modify_time, hidden
from threads, users from threads, users
where thread_id = ? and author_id = user_id where thread_id = ? and author_id = user_id
''', ''',
(thread,) (thread,)
).fetchone() ).fetchone()
comments = db.execute(''' comments = db.execute('''
select comment_id, parent_id, author_id, name, text, create_time, modify_time select
comment_id,
parent_id,
author_id,
name,
text,
create_time,
modify_time,
hidden
from comments from comments
left join users left join users
on author_id = user_id on author_id = user_id
@@ -67,7 +100,7 @@ class DB:
''', ''',
(thread,) (thread,)
) )
return title, text, author, author_id, create_time, modify_time, comments return title, text, author, author_id, create_time, modify_time, comments, hidden
def get_thread_title(self, thread_id): def get_thread_title(self, thread_id):
return self._db().execute(''' return self._db().execute('''
@@ -123,8 +156,21 @@ class DB:
union union
select comment_id from descendant_of, comments where id = parent_id select comment_id from descendant_of, comments where id = parent_id
) )
select id, parent_id, author_id, name, text, create_time, modify_time from descendant_of, comments, users select
where id = comment_id and user_id = author_id id,
parent_id,
author_id,
name,
text,
create_time,
modify_time,
hidden
from
descendant_of,
comments,
users
where id = comment_id
and user_id = author_id
''', ''',
(comment_id,) (comment_id,)
) )
@@ -475,6 +521,24 @@ class DB:
(role, user_id) (role, user_id)
) )
def set_thread_hidden(self, thread_id, hide):
return self.change_one('''
update threads
set hidden = ?
where thread_id = ?
''',
(hide, thread_id)
)
def set_comment_hidden(self, comment_id, hide):
return self.change_one('''
update comments
set hidden = ?
where comment_id = ?
''',
(hide, comment_id)
)
def change_one(self, query, values): def change_one(self, query, values):
db = self._db() db = self._db()
c = db.cursor() c = db.cursor()

View File

@@ -38,7 +38,7 @@ $SQLITE "$1" -init schema.txt "insert into config (
registration_enabled registration_enabled
) )
values ( values (
'agreper-v0.1', 'agreper-v0.1.1',
'Agreper', 'Agreper',
'', '',
'$(head -c 30 /dev/urandom | base64)', '$(head -c 30 /dev/urandom | base64)',

83
main.py
View File

@@ -1,4 +1,4 @@
VERSION = 'agreper-v0.1' VERSION = 'agreper-v0.1.1'
# TODO put in config table # TODO put in config table
THREADS_PER_PAGE = 50 THREADS_PER_PAGE = 50
@@ -42,7 +42,8 @@ def index():
def forum(forum_id): def forum(forum_id):
title, description = db.get_forum(forum_id) title, description = db.get_forum(forum_id)
offset = int(request.args.get('p', 0)) offset = int(request.args.get('p', 0))
threads = [*db.get_threads(forum_id, offset, THREADS_PER_PAGE + 1)] user_id = session.get('user_id', -1)
threads = [*db.get_threads(forum_id, offset, THREADS_PER_PAGE + 1, user_id)]
if len(threads) == THREADS_PER_PAGE + 1: if len(threads) == THREADS_PER_PAGE + 1:
threads.pop() threads.pop()
next_page = offset + THREADS_PER_PAGE next_page = offset + THREADS_PER_PAGE
@@ -62,18 +63,19 @@ def forum(forum_id):
@app.route('/thread/<int:thread_id>/') @app.route('/thread/<int:thread_id>/')
def thread(thread_id): def thread(thread_id):
user_id = session.get('user_id') user = get_user()
title, text, author, author_id, create_time, modify_time, comments = db.get_thread(thread_id) title, text, author, author_id, create_time, modify_time, comments, hidden = db.get_thread(thread_id)
comments = create_comment_tree(comments) comments = create_comment_tree(comments, user)
return render_template( return render_template(
'thread.html', 'thread.html',
title = title, title = title,
config = config, config = config,
user = get_user(), user = user,
text = text, text = text,
author = author, author = author,
author_id = author_id, author_id = author_id,
thread_id = thread_id, thread_id = thread_id,
hidden = hidden,
create_time = create_time, create_time = create_time,
modify_time = modify_time, modify_time = modify_time,
comments = comments, comments = comments,
@@ -81,8 +83,9 @@ def thread(thread_id):
@app.route('/comment/<int:comment_id>/') @app.route('/comment/<int:comment_id>/')
def comment(comment_id): def comment(comment_id):
user = get_user()
thread_id, parent_id, title, comments = db.get_subcomments(comment_id) thread_id, parent_id, title, comments = db.get_subcomments(comment_id)
comments = create_comment_tree(comments) comments = create_comment_tree(comments, user)
reply_comment, = comments reply_comment, = comments
comments = reply_comment.children comments = reply_comment.children
reply_comment.children = [] reply_comment.children = []
@@ -90,7 +93,7 @@ def comment(comment_id):
'comments.html', 'comments.html',
title = title, title = title,
config = config, config = config,
user = get_user(), user = user,
reply_comment = reply_comment, reply_comment = reply_comment,
comments = comments, comments = comments,
parent_id = parent_id, parent_id = parent_id,
@@ -574,6 +577,42 @@ def admin_restart():
restart() restart()
return redirect(url_for('admin')) return redirect(url_for('admin'))
@app.route('/thread/<int:thread_id>/hide/', methods = ['POST'])
def set_hide_thread(thread_id):
chk, user = _moderator_check()
if not chk:
return user
try:
hide = request.form['hide'] != '0'
hide_str = 'Hidden' if hide else 'Unhidden'
if db.set_thread_hidden(thread_id, hide):
flash(f'{hide_str} thread', 'success')
else:
flash(f'Failed to {hide_str.lower()} thread', 'error')
except Exception as e:
flash(str(e), 'error')
return redirect(request.form['redirect'])
@app.route('/comment/<int:comment_id>/hide/', methods = ['POST'])
def set_hide_comment(comment_id):
chk, user = _moderator_check()
if not chk:
return user
try:
hide = request.form['hide'] != '0'
hide_str = 'Hidden' if hide else 'Unhidden'
if db.set_comment_hidden(comment_id, hide):
flash(f'{hide_str} comment', 'success')
else:
flash(f'Failed to {hide_str.lower()} comment', 'error')
except Exception as e:
flash(str(e), 'error')
return redirect(request.form['redirect'])
# TODO can probably be a static-esque page, maybe? # TODO can probably be a static-esque page, maybe?
@app.route('/help/') @app.route('/help/')
def help(): def help():
@@ -601,7 +640,7 @@ def _admin_check():
class Comment: class Comment:
def __init__(self, id, author_id, author, text, create_time, modify_time, parent_id): def __init__(self, id, parent_id, author_id, author, text, create_time, modify_time, hidden):
self.id = id self.id = id
self.author_id = author_id self.author_id = author_id
self.author = author self.author = author
@@ -610,23 +649,35 @@ class Comment:
self.create_time = create_time self.create_time = create_time
self.modify_time = modify_time self.modify_time = modify_time
self.parent_id = parent_id self.parent_id = parent_id
self.hidden = hidden
def create_comment_tree(comments): def create_comment_tree(comments, user):
start = time.time(); start = time.time();
# Collect comments first, then build the tree in case we encounter a child before a parent # Collect comments first, then build the tree in case we encounter a child before a parent
comment_map = { comment_map = { v[0]: Comment(*v) for v in comments }
comment_id: Comment(comment_id, author_id, author, text, create_time, modify_time, parent_id)
for comment_id, parent_id, author_id, author, text, create_time, modify_time
in comments
}
root = [] root = []
# We should keep showing hidden comments if the user replied to them, directly or indirectly.
# To do that, keep track of user comments, then walk up the tree and insert hidden comments.
user_comments = []
# Build tree # Build tree
for comment in comment_map.values(): def insert(comment):
parent = comment_map.get(comment.parent_id) parent = comment_map.get(comment.parent_id)
if parent is not None: if parent is not None:
parent.children.append(comment) parent.children.append(comment)
else: else:
root.append(comment) root.append(comment)
for comment in comment_map.values():
if comment.hidden and (not user or not user.is_moderator()):
continue
insert(comment)
if user and (comment.author_id == user.id and not user.is_moderator()):
user_comments.append(comment)
# Insert replied-to hidden comments
for c in user_comments:
while c is not None:
if c.hidden:
insert(c)
c = comment_map.get(c.parent_id)
# Sort each comment based on create time # Sort each comment based on create time
def sort_time(l): def sort_time(l):
l.sort(key=lambda c: c.modify_time, reverse=True) l.sort(key=lambda c: c.modify_time, reverse=True)

View File

@@ -27,7 +27,7 @@ create table threads (
title varchar(64) not null, title varchar(64) not null,
text text not null, text text not null,
score integer not null default 0, score integer not null default 0,
dead boolean not null default false hidden boolean not null default false
); );
create table comments ( create table comments (
@@ -39,7 +39,7 @@ create table comments (
modify_time integer not null, modify_time integer not null,
text text not null, text text not null,
score integer not null default 0, score integer not null default 0,
dead boolean not null default false hidden boolean not null default false
); );
create table forums ( create table forums (

View File

@@ -1,10 +1,13 @@
{% from 'moderator.html' import moderate_comment with context -%}
{%- macro author(id, name, ctime, mtime) -%} {%- macro author(id, name, ctime, mtime) -%}
<i><a href="{{ url_for('user_info', user_id = id) }}">{{ name }}</a> - {{ format_since(ctime) }}{% if ctime != mtime %} (last modified {{ format_since(mtime) }}){% endif %}</i> <i><a href="{{ url_for('user_info', user_id = id) }}">{{ name }}</a> - {{ format_since(ctime) }}{% if ctime != mtime %} (last modified {{ format_since(mtime) }}){% endif %}</i>
{%- endmacro -%} {%- endmacro -%}
{%- macro comment_author(comment, thread_id, can_delete) -%} {%- macro comment_author(comment, thread_id, can_delete) -%}
<span class=small> <span class=small>
{{- author(comment.author_id, comment.author, comment.create_time, comment.modify_time) }} | {{- '[hidden]' if comment.hidden else '' }}
{{ author(comment.author_id, comment.author, comment.create_time, comment.modify_time) }} |
{# Suffixing a # prevents unnecessary reloads #} {# Suffixing a # prevents unnecessary reloads #}
<a href="{{ url_for('thread', thread_id = thread_id) }}#"> thread</a> <a href="{{ url_for('thread', thread_id = thread_id) }}#"> thread</a>
{%- if comment.parent_id is not none -%} {%- if comment.parent_id is not none -%}
@@ -15,6 +18,9 @@
{%- if can_delete -%} {%- if can_delete -%}
<a href="{{ url_for('confirm_delete_comment', comment_id = comment.id) }}"> delete</a> <a href="{{ url_for('confirm_delete_comment', comment_id = comment.id) }}"> delete</a>
{%- endif -%} {%- endif -%}
{%- if user.is_moderator() -%}
{{ moderate_comment(comment.id, comment.hidden) }}
{%- endif -%}
{%- endif -%} {%- endif -%}
</span> </span>
{%- endmacro -%} {%- endmacro -%}

View File

@@ -1,4 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{%- from 'moderator.html' import moderate_thread with context %}
{%- macro nav() -%} {%- macro nav() -%}
<p style=text-align:center> <p style=text-align:center>
@@ -19,14 +20,22 @@
<th>Created</th> <th>Created</th>
<th>Updated</th> <th>Updated</th>
<th>Comments</th> <th>Comments</th>
{%- if user is not none and user.is_moderator() -%}
<th>Action</th>
{%- endif -%}
</tr> </tr>
{% for id, title, ctime, utime, author_id, author, comment_count in threads %} {% for id, title, ctime, utime, author_id, author, comment_count, hidden in threads %}
<tr> <tr>
<th><a href="{{ url_for('thread', thread_id = id) }}">{{ title }}</a></th> <th>{{ '[hidden] ' if hidden else '' }}<a href="{{ url_for('thread', thread_id = id) }}">{{ title }}</a></th>
<td><a href="{{ url_for('user_info', user_id = author_id) }}">{{ author }}</a></td> <td><a href="{{ url_for('user_info', user_id = author_id) }}">{{ author }}</a></td>
<td>{{ format_since(ctime) }}</td> <td>{{ format_since(ctime) }}</td>
<td>{{ format_since(utime) }}</td> <td>{{ format_since(utime) }}</td>
<td>{{ comment_count }}</td> <td>{{ comment_count }}</td>
<td>
{%- if user is not none and user.is_moderator() %}
{{- moderate_thread(id, hidden) }}
{%- endif -%}
</td>
</tr> </tr>
{%- endfor -%} {%- endfor -%}
</table> </table>

15
templates/moderator.html Normal file
View File

@@ -0,0 +1,15 @@
{% macro moderate_thread(id, hidden) %}
<form method=post action="{{ url_for('set_hide_thread', thread_id = id) }}">
<input name=redirect value="{{ request.full_path }}" hidden>
<input name=hide value={{ 0 if hidden else 1 }} hidden>
<input type=submit value="{{ 'Unhide' if hidden else 'Hide' }}">
</form>
{% endmacro %}
{% macro moderate_comment(id, hidden) %}
<form method=post action="{{ url_for('set_hide_comment', comment_id = id) }}" style=display:inline>
<input name=redirect value="{{ request.full_path }}" hidden>
<input name=hide value={{ 0 if hidden else 1 }} hidden>
<input type=submit value="{{ 'Unhide' if hidden else 'Hide' }}">
</form>
{% endmacro %}

View File

@@ -1,8 +1,12 @@
{%- extends 'base.html' %} {%- extends 'base.html' %}
{%- from 'comment.html' import render_comment, reply, thread_author with context %} {%- from 'comment.html' import render_comment, reply, thread_author with context %}
{%- from 'moderator.html' import moderate_thread with context %}
{%- block content %} {%- block content %}
{{ thread_author(author_id, author, create_time, modify_time) }} {%- if user is not none and user.is_moderator() -%}
<p>{{ moderate_thread(thread_id, hidden) }}</p>
{%- endif -%}
{{- thread_author(author_id, author, create_time, modify_time) }}
<p>{{ minimd(text) | safe }}</p> <p>{{ minimd(text) | safe }}</p>
{{- reply() }} {{- reply() }}

9
upgrade/sqlite/v0.1.sh Normal file
View File

@@ -0,0 +1,9 @@
set -ex
test $# == 1
"$SQLITE" "$1" "
begin exclusive;
alter table threads rename dead to hidden;
alter table comments rename dead to hidden;
update config set version = 'agreper-v0.1.1';
end;
"

86
upgrade_sqlite.sh Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Script to upgrade a database from one version to another by adding columns,
# tables etc.
# Upgrade scripts go into upgrade/sqlite/
# If there are multiple changes after a revision but before a new one, suffix a
# letter (e.g. `v0.1.1a`).
# When a new revision is out, add a script that changes just the version.
LAST_VERSION=agreper-v0.1.1
SQLITE=sqlite3
export SQLITE
set -e
if [ $# -lt 1 ]
then
echo "Usage: $0 <file.db> [--no-backup]" >&2
exit 1
fi
make_backup=0
if [ $# -ge 2 ]
then
case "$2" in
--no-backup)
make_backup=1
;;
*)
echo "Unknown option $2"
exit 1
;;
esac
fi
if ! [ -f "$1" ]
then
echo "Database '$1' doesn't exist" >&2
exit 1
fi
version=$(sqlite3 "$1" 'select version from config')
while true
do
case "$version" in
# Last version, do nothing
agreper-v0.1.1)
echo "$version is the latest version"
exit 0
;;
# Try to upgrade
agreper-*)
echo "Upgrading from $version"
if [ $make_backup ]
then
backup="$1.bak-$version"
if [ -f "$backup" ]
then
echo "Backup '$backup' already exists (did a previous upgrade fail?)" >&2
exit 1
fi
echo "Creating backup of $1 at $backup"
cp --reflink=auto "$1" "$backup"
make_backup=1
fi
script="./upgrade/sqlite/${version#agreper-}.sh"
if ! bash "$script" "$1"
then
echo "Error while executing $script"
exit 1
fi
;;
# Unrecognized version
*)
echo "Unknown version $version" >&2
exit 1
;;
esac
version=$(sqlite3 "$1" 'select version from config')
done