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.
## Upgrading
To upgrade from a previous version, run ``upgrade_sqlite.sh``
## Screenshots
![Index](https://static.agreper.com/index.png)

View File

@@ -19,7 +19,7 @@ class DB:
on t.thread_id = (
select tt.thread_id
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
limit 1
)
@@ -35,31 +35,64 @@ class DB:
(forum_id,)
).fetchone()
def get_threads(self, forum_id, offset, limit):
def get_threads(self, forum_id, offset, limit, user_id):
return self._db().execute('''
select t.thread_id, title, t.create_time, t.update_time, t.author_id, name, count(c.thread_id)
from threads t, users
left join comments c on t.thread_id = c.thread_id
where forum_id = ? and user_id = t.author_id
select
t.thread_id,
title,
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
order by t.update_time desc
limit ?
offset ?
''',
(forum_id, limit, offset)
(forum_id, user_id, limit, offset)
)
def get_thread(self, thread):
db = self._db()
title, text, author, author_id, create_time, modify_time = db.execute('''
select title, text, name, author_id, create_time, modify_time
title, text, author, author_id, create_time, modify_time, hidden = db.execute('''
select title, text, name, author_id, create_time, modify_time, hidden
from threads, users
where thread_id = ? and author_id = user_id
''',
(thread,)
).fetchone()
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
left join users
on author_id = user_id
@@ -67,7 +100,7 @@ class DB:
''',
(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):
return self._db().execute('''
@@ -123,8 +156,21 @@ class DB:
union
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
where id = comment_id and user_id = author_id
select
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,)
)
@@ -475,6 +521,24 @@ class DB:
(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):
db = self._db()
c = db.cursor()

View File

@@ -38,7 +38,7 @@ $SQLITE "$1" -init schema.txt "insert into config (
registration_enabled
)
values (
'agreper-v0.1',
'agreper-v0.1.1',
'Agreper',
'',
'$(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
THREADS_PER_PAGE = 50
@@ -42,7 +42,8 @@ def index():
def forum(forum_id):
title, description = db.get_forum(forum_id)
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:
threads.pop()
next_page = offset + THREADS_PER_PAGE
@@ -62,18 +63,19 @@ def forum(forum_id):
@app.route('/thread/<int:thread_id>/')
def thread(thread_id):
user_id = session.get('user_id')
title, text, author, author_id, create_time, modify_time, comments = db.get_thread(thread_id)
comments = create_comment_tree(comments)
user = get_user()
title, text, author, author_id, create_time, modify_time, comments, hidden = db.get_thread(thread_id)
comments = create_comment_tree(comments, user)
return render_template(
'thread.html',
title = title,
config = config,
user = get_user(),
user = user,
text = text,
author = author,
author_id = author_id,
thread_id = thread_id,
hidden = hidden,
create_time = create_time,
modify_time = modify_time,
comments = comments,
@@ -81,8 +83,9 @@ def thread(thread_id):
@app.route('/comment/<int:comment_id>/')
def comment(comment_id):
user = get_user()
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
comments = reply_comment.children
reply_comment.children = []
@@ -90,7 +93,7 @@ def comment(comment_id):
'comments.html',
title = title,
config = config,
user = get_user(),
user = user,
reply_comment = reply_comment,
comments = comments,
parent_id = parent_id,
@@ -574,6 +577,42 @@ def admin_restart():
restart()
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?
@app.route('/help/')
def help():
@@ -601,7 +640,7 @@ def _admin_check():
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.author_id = author_id
self.author = author
@@ -610,23 +649,35 @@ class Comment:
self.create_time = create_time
self.modify_time = modify_time
self.parent_id = parent_id
self.hidden = hidden
def create_comment_tree(comments):
def create_comment_tree(comments, user):
start = time.time();
# Collect comments first, then build the tree in case we encounter a child before a parent
comment_map = {
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
}
comment_map = { v[0]: Comment(*v) for v in comments }
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
for comment in comment_map.values():
def insert(comment):
parent = comment_map.get(comment.parent_id)
if parent is not None:
parent.children.append(comment)
else:
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
def sort_time(l):
l.sort(key=lambda c: c.modify_time, reverse=True)

View File

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

View File

@@ -1,10 +1,13 @@
{% from 'moderator.html' import moderate_comment with context -%}
{%- 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>
{%- endmacro -%}
{%- macro comment_author(comment, thread_id, can_delete) -%}
<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 #}
<a href="{{ url_for('thread', thread_id = thread_id) }}#"> thread</a>
{%- if comment.parent_id is not none -%}
@@ -15,6 +18,9 @@
{%- if can_delete -%}
<a href="{{ url_for('confirm_delete_comment', comment_id = comment.id) }}"> delete</a>
{%- endif -%}
{%- if user.is_moderator() -%}
{{ moderate_comment(comment.id, comment.hidden) }}
{%- endif -%}
{%- endif -%}
</span>
{%- endmacro -%}

View File

@@ -1,4 +1,5 @@
{% extends 'base.html' %}
{%- from 'moderator.html' import moderate_thread with context %}
{%- macro nav() -%}
<p style=text-align:center>
@@ -19,14 +20,22 @@
<th>Created</th>
<th>Updated</th>
<th>Comments</th>
{%- if user is not none and user.is_moderator() -%}
<th>Action</th>
{%- endif -%}
</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>
<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>{{ format_since(ctime) }}</td>
<td>{{ format_since(utime) }}</td>
<td>{{ comment_count }}</td>
<td>
{%- if user is not none and user.is_moderator() %}
{{- moderate_thread(id, hidden) }}
{%- endif -%}
</td>
</tr>
{%- endfor -%}
</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' %}
{%- from 'comment.html' import render_comment, reply, thread_author with context %}
{%- from 'moderator.html' import moderate_thread with context %}
{%- 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>
{{- 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