Merge pull request #9 from Demindiro/mod-hide-threads
This commit is contained in:
@@ -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
|
||||
|
||||

|
||||
|
||||
90
db/sqlite.py
90
db/sqlite.py
@@ -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()
|
||||
|
||||
@@ -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
83
main.py
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 -%}
|
||||
|
||||
@@ -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
15
templates/moderator.html
Normal 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 %}
|
||||
@@ -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
9
upgrade/sqlite/v0.1.sh
Normal 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
86
upgrade_sqlite.sh
Executable 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
|
||||
Reference in New Issue
Block a user