aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorubq323 <ubq323>2021-08-18 23:54:49 +0000
committerubq323 <ubq323>2021-08-18 23:54:49 +0000
commitd2e51e7b8bb2f765e79c98f13165b0faf59a302c (patch)
tree8b5972c5e05d44cfabca011fab50399c04ca5456
parentc51c87b6d012c2a250054b47e330e1c504aebb4a (diff)
parent17357a3fb5cf603ff79daad644f4a4c0fbe42150 (diff)
merge trunk into this
-rw-r--r--apioforum/__init__.py5
-rw-r--r--apioforum/db.py36
-rw-r--r--apioforum/forum.py262
-rw-r--r--apioforum/roles.py97
-rw-r--r--apioforum/static/style.css66
-rw-r--r--apioforum/templates/common.html53
-rw-r--r--apioforum/templates/config_thread.html5
-rw-r--r--apioforum/templates/delete_thread.html18
-rw-r--r--apioforum/templates/edit_forum.html25
-rw-r--r--apioforum/templates/edit_permissions.html94
-rw-r--r--apioforum/templates/role_assignment.html56
-rw-r--r--apioforum/templates/view_forum.html80
-rw-r--r--apioforum/templates/view_thread.html33
-rw-r--r--apioforum/templates/view_unlisted.html24
-rw-r--r--apioforum/thread.py224
-rw-r--r--apioforum/user.py7
16 files changed, 923 insertions, 162 deletions
diff --git a/apioforum/__init__.py b/apioforum/__init__.py
index e20f67b..087df81 100644
--- a/apioforum/__init__.py
+++ b/apioforum/__init__.py
@@ -54,6 +54,11 @@ def create_app():
return dict(path_for_next=p)
app.jinja_env.globals.update(forum_path=forum.forum_path)
+ from .roles import has_permission, is_bureaucrat, get_user_role
+ app.jinja_env.globals.update(
+ has_permission=has_permission,
+ is_bureaucrat=is_bureaucrat,
+ get_user_role=get_user_role)
from .mdrender import render
@app.template_filter('md')
diff --git a/apioforum/db.py b/apioforum/db.py
index 50b142e..c0c8c7e 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -171,6 +171,42 @@ DROP TABLE tags;
ALTER TABLE tags_new RENAME TO tags;
PRAGMA foreign_keys = on;
""",
+"""
+CREATE TABLE role_config (
+ role TEXT NOT NULL,
+ forum NOT NULL REFERENCES forums(id),
+ id INTEGER PRIMARY KEY,
+
+ p_create_threads INT NOT NULL DEFAULT 1,
+ p_reply_threads INT NOT NULL DEFAULT 1,
+ p_view_threads INT NOT NULL DEFAULT 1,
+ p_manage_threads INT NOT NULL DEFAULT 0,
+ p_delete_posts INT NOT NULL DEFAULT 0,
+ p_vote INT NOT NULL DEFAULT 1,
+ p_create_polls INT NOT NULL DEFAULT 1,
+ p_approve INT NOT NULL DEFAULT 0,
+ p_create_subforum INT NOT NULL DEFAULT 0
+);
+
+INSERT INTO role_config (role,forum) VALUES ("approved",1);
+INSERT INTO role_config (role,forum) VALUES ("other",1);
+""",
+"""
+CREATE TABLE role_assignments (
+ user NOT NULL REFERENCES users(username),
+ forum NOT NULL REFERENCES forums(id),
+ role TEXT NOT NULL
+);
+""",
+"""
+ALTER TABLE posts ADD COLUMN deleted NOT NULL DEFAULT 0;
+""",
+"""
+ALTER TABLE forums ADD COLUMN unlisted NOT NULL DEFAULT 0;
+""",
+"""
+ALTER TABLE role_config ADD COLUMN p_view_forum INT NOT NULL DEFAULT 1;
+"""
]
def init_db():
diff --git a/apioforum/forum.py b/apioforum/forum.py
index 3a5dbe3..87d4022 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -1,5 +1,6 @@
# view threads in a forum
# currently there is only ever one forum however
+# ^ aha we never removed this. we should keep it. it is funny.
from flask import (
Blueprint, render_template, request,
@@ -8,10 +9,12 @@ from flask import (
from .db import get_db
from .mdrender import render
-
+from .roles import get_forum_roles,has_permission,is_bureaucrat,get_user_role, permissions as role_permissions
+from .permissions import is_admin
from sqlite3 import OperationalError
import datetime
import math
+import functools
THREADS_PER_PAGE = 20
@@ -46,13 +49,53 @@ def forum_path(forum_id):
ancestors.reverse()
return ancestors
-@bp.route("/<int:forum_id>")
-@bp.route("/<int:forum_id>/page/<int:page>")
-def view_forum(forum_id,page=1):
+def forum_route(relative_path, pagination=False, **kwargs):
+ def decorator(f):
+ path = "/<int:forum_id>"
+ if relative_path != "":
+ path += "/" + relative_path
+
+ @bp.route(path, **kwargs)
+ @functools.wraps(f)
+ def wrapper(forum_id, *args, **kwargs):
+ db = get_db()
+ forum = db.execute("SELECT * FROM forums WHERE id = ?",
+ (forum_id,)).fetchone()
+ if forum == None:
+ abort(404)
+ return f(forum, *args, **kwargs)
+
+ if pagination:
+ wrapper = bp.route(path+"/page/<int:page>", **kwargs)(wrapper)
+
+ return decorator
+
+def requires_permission(permission, login_required=True):
+ def decorator(f):
+ @functools.wraps(f)
+ def wrapper(forum, *args, **kwargs):
+ if not has_permission(forum['id'],g.user,permission,login_required):
+ abort(403)
+ return f(forum, *args, **kwargs)
+ return wrapper
+ return decorator
+
+def requires_bureaucrat(f):
+ @functools.wraps(f)
+ @requires_permission("p_view_forum")
+ def wrapper(forum, *args, **kwargs):
+ if not is_bureaucrat(forum['id'], g.user):
+ abort(403)
+ return f(forum, *args, **kwargs)
+ return wrapper
+
+
+@forum_route("",pagination=True)
+@requires_permission("p_view_forum", login_required=False)
+def view_forum(forum,page=1):
if page < 1:
abort(400)
db = get_db()
- forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum_id,)).fetchone()
threads = db.execute(
"""SELECT
threads.id, threads.title, threads.creator, threads.created,
@@ -60,7 +103,8 @@ def view_forum(forum_id,page=1):
most_recent_posts.created as mrp_created,
most_recent_posts.author as mrp_author,
most_recent_posts.id as mrp_id,
- most_recent_posts.content as mrp_content
+ most_recent_posts.content as mrp_content,
+ most_recent_posts.deleted as mrp_deleted
FROM threads
INNER JOIN most_recent_posts ON most_recent_posts.thread = threads.id
INNER JOIN number_of_posts ON number_of_posts.thread = threads.id
@@ -68,19 +112,19 @@ def view_forum(forum_id,page=1):
ORDER BY threads.updated DESC
LIMIT ? OFFSET ?;
""",(
- forum_id,
+ forum['id'],
THREADS_PER_PAGE,
(page-1)*THREADS_PER_PAGE,
)).fetchall()
# XXX: update this when thread filtering happens
- num_threads = db.execute("SELECT count(*) AS count FROM threads WHERE threads.forum = ?",(forum_id,)).fetchone()['count']
+ num_threads = db.execute("SELECT count(*) AS count FROM threads WHERE threads.forum = ?",(forum['id'],)).fetchone()['count']
max_pageno = math.ceil(num_threads/THREADS_PER_PAGE)
thread_tags = {}
thread_polls = {}
- avail_tags = get_avail_tags(forum_id)
+ avail_tags = get_avail_tags(forum['id'])
#todo: somehow optimise this
for thread in threads:
@@ -117,33 +161,43 @@ def view_forum(forum_id,page=1):
subforums_rows = db.execute("""
SELECT max(threads.updated) as updated, forums.* FROM forums
LEFT OUTER JOIN threads ON threads.forum=forums.id
- WHERE parent = ?
+ WHERE parent = ? AND unlisted = 0
GROUP BY forums.id
ORDER BY name ASC
- """,(forum_id,)).fetchall()
+ """,(forum['id'],)).fetchall()
subforums = []
for s in subforums_rows:
a={}
a.update(s)
if a['updated'] is not None:
a['updated'] = datetime.datetime.fromisoformat(a['updated'])
- subforums.append(a)
+ if has_permission(a['id'],g.user,"p_view_forum",login_required=False):
+ subforums.append(a)
+
+ bureaucrats = db.execute("""
+ SELECT user FROM role_assignments
+ WHERE role = 'bureaucrat' AND forum = ?
+ """,(forum['id'],)).fetchall()
+ bureaucrats = [b[0] for b in bureaucrats]
return render_template("view_forum.html",
forum=forum,
subforums=subforums,
threads=threads,
thread_tags=thread_tags,
+ bureaucrats=bureaucrats,
thread_polls=thread_polls,
avail_tags=avail_tags,
max_pageno=max_pageno,
page=page,
)
-@bp.route("/<int:forum_id>/create_thread",methods=("GET","POST"))
-def create_thread(forum_id):
+@forum_route("create_thread",methods=("GET","POST"))
+@requires_permission("p_create_threads")
+@requires_permission("p_view_forum")
+def create_thread(forum):
db = get_db()
- forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum_id,)).fetchone()
+ forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum['id'],)).fetchone()
if forum is None:
flash("that forum doesn't exist")
return redirect(url_for('index'))
@@ -163,7 +217,7 @@ def create_thread(forum_id):
cur = db.cursor()
cur.execute(
"INSERT INTO threads (title,creator,created,updated,forum) VALUES (?,?,current_timestamp,current_timestamp,?);",
- (title,g.user,forum_id)
+ (title,g.user,forum['id'])
)
thread_id = cur.lastrowid
cur.execute(
@@ -177,6 +231,182 @@ def create_thread(forum_id):
return render_template("create_thread.html")
+@forum_route("roles",methods=("GET","POST"))
+@requires_bureaucrat
+def edit_roles(forum):
+ db = get_db()
+ role_configs = db.execute(
+ "SELECT * FROM role_config WHERE forum = ? ORDER BY ID ASC",
+ (forum['id'],)).fetchall()
+
+ if request.method == "POST":
+ for config in role_configs:
+ if 'delete_' + config['role'] in request.form:
+ db.execute(
+ "DELETE FROM role_config WHERE forum = ? AND role = ?",
+ (forum['id'],config['role']))
+ elif 'roleconfig_' + config['role'] in request.form:
+ for p in role_permissions:
+ permission_setting =\
+ f"perm_{config['role']}_{p}" in request.form
+ db.execute(f"""
+ UPDATE role_config SET {p} = ?
+ WHERE forum = ? AND role = ?;
+ """,
+ (permission_setting,forum['id'], config['role']))
+ db.commit()
+ flash('roles sucessfully enroled')
+ return redirect(url_for('forum.view_forum',forum_id=forum['id']))
+
+ role_config_roles = [c['role'] for c in role_configs]
+ other_roles = [role for role in get_forum_roles(forum['id']) if not role in role_config_roles]
+
+ return render_template("edit_permissions.html",
+ forum=forum,
+ role_configs=role_configs,
+ other_roles=other_roles
+ )
+
+@forum_route("roles/new",methods=["POST"])
+@requires_bureaucrat
+def add_role(forum):
+ name = request.form['role'].strip()
+ if not all(c in (" ","-","_") or c.isalnum() for c in name) \
+ or len(name) > 32:
+ flash("role name must contain no special characters")
+ return redirect(url_for('forum.edit_roles',forum_id=forum['id']))
+ if name == "bureaucrat":
+ flash("cannot configure permissions for bureaucrat")
+ return redirect(url_for('forum.edit_roles',forum_id=forum['id']))
+
+ db = get_db()
+
+ existing_config = db.execute("""
+ SELECT * FROM role_config WHERE forum = ? AND role = ?
+ """,(forum['id'],name)).fetchone()
+ if not existing_config:
+ db.execute("INSERT INTO role_config (forum,role) VALUES (?,?)",
+ (forum['id'],name))
+ db.commit()
+ return redirect(url_for('forum.edit_roles',forum_id=forum['id']))
+
+@forum_route("role",methods=["GET","POST"])
+@requires_permission("p_approve")
+def view_user_role(forum):
+ if request.method == "POST":
+ return redirect(url_for( 'forum.edit_user_role',
+ username=request.form['user'],forum_id=forum['id']))
+ else:
+ return render_template("role_assignment.html",forum=forum)
+
+@forum_route("role/<username>",methods=["GET","POST"])
+@requires_permission("p_approve")
+def edit_user_role(forum, username):
+ db = get_db()
+ if request.method == "POST":
+ user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
+ if user == None:
+ return redirect(url_for('forum.edit_user_role',
+ username=username,forum_id=forum['id']))
+ role = request.form['role']
+ if role not in get_forum_roles(forum['id']) and role != "" and role != "bureaucrat":
+ flash("no such role")
+ return redirect(url_for('forum.edit_user_role',
+ username=username,forum_id=forum['id']))
+ if not is_bureaucrat(forum['id'],g.user) and role != "approved" and role != "":
+ # only bureaucrats can assign arbitrary roles
+ abort(403)
+ existing = db.execute(
+ "SELECT * FROM role_assignments WHERE user = ? AND forum = ?;",
+ (username,forum['id'])).fetchone()
+ if existing:
+ db.execute("DELETE FROM role_assignments WHERE user = ? AND forum = ?;",(username,forum['id']))
+ if role != "":
+ db.execute(
+ "INSERT INTO role_assignments (user,role,forum) VALUES (?,?,?);",
+ (username,role,forum['id']))
+ db.commit()
+ flash("role assigned assignedly")
+ return redirect(url_for('forum.view_forum',forum_id=forum['id']))
+ else:
+ user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
+ if user == None:
+ return render_template("role_assignment.html",
+ forum=forum,user=username,invalid_user=True)
+ r = db.execute(
+ "SELECT role FROM role_assignments WHERE user = ? AND forum = ?;",
+ (username,forum['id'])).fetchone()
+ if not r:
+ assigned_role = ""
+ else:
+ assigned_role = r[0]
+ role = get_user_role(forum['id'], username)
+ if is_bureaucrat(forum['id'], g.user):
+ roles = get_forum_roles(forum['id'])
+ roles.remove("other")
+ roles.add("bureaucrat")
+ else:
+ roles = ["approved"]
+ return render_template("role_assignment.html",
+ forum=forum,user=username,role=role,
+ assigned_role=assigned_role,forum_roles=roles)
+
+def forum_config_page(forum, create=False):
+ db = get_db()
+ if request.method == "POST":
+ name = request.form["name"]
+ desc = request.form["description"]
+ if len(name) > 100 or len(name.strip()) == 0:
+ flash("invalid name")
+ return redirect(url_for('forum.edit_forum',forum_id=forum['id']))
+ elif len(desc) > 6000:
+ flash("invalid description")
+ return redirect(url_for('forum.edit_forum',forum_id=forum['id']))
+ if not create:
+ db.execute("UPDATE forums SET name = ?, description = ? WHERE id = ?",
+ (name,desc,forum['id']))
+ fid = forum['id']
+ else:
+ cur = db.cursor()
+ cur.execute(
+ "INSERT INTO forums (name,description,parent) VALUES (?,?,?)",
+ (name,desc,forum['id']))
+ new = cur.lastrowid
+ # creator becomes bureaucrat of new forum
+ db.execute("INSERT INTO role_assignments (role,user,forum) VALUES (?,?,?)",
+ ("bureaucrat",g.user,new))
+ fid = new
+ db.commit()
+ return redirect(url_for('forum.view_forum',forum_id=fid))
+ else:
+ if create:
+ name = ""
+ desc = ""
+ else:
+ name = forum['name']
+ desc = forum['description']
+ cancel_link = url_for('forum.view_forum',forum_id=forum['id'])
+ return render_template("edit_forum.html",create=create,
+ name=name,description=desc,cancel_link=cancel_link)
+
+@forum_route("edit",methods=["GET","POST"])
+@requires_bureaucrat
+def edit_forum(forum):
+ return forum_config_page(forum)
+
+@forum_route("create",methods=["GET","POST"])
+@requires_permission("p_create_subforum")
+def create_forum(forum):
+ return forum_config_page(forum,create=True)
+
+#@forum_route("unlisted")
+#def view_unlisted(forum):
+# if not is_admin: abort(403) # why doesn't this fucking work
+# db = get_db()
+# unlisted = db.execute(
+# "SELECT * FROM forums WHERE unlisted = 1 AND parent = ?",(forum['id'],))
+# return render_template('view_unlisted.html',forum=forum,unlisted=unlisted)
+
@bp.route("/search")
def search():
db = get_db()
diff --git a/apioforum/roles.py b/apioforum/roles.py
new file mode 100644
index 0000000..aa1d239
--- /dev/null
+++ b/apioforum/roles.py
@@ -0,0 +1,97 @@
+
+from .db import get_db
+from .permissions import is_admin
+
+permissions = [
+ "p_create_threads",
+ "p_reply_threads",
+ "p_manage_threads",
+ "p_delete_posts",
+ "p_view_threads",
+ "p_vote",
+ "p_create_polls",
+ "p_approve",
+ "p_create_subforum",
+ "p_view_forum"
+]
+
+def get_role_config(forum_id, role):
+ db = get_db()
+
+ fid = forum_id
+ the = None
+ while the == None and fid != None:
+ the = db.execute("""
+ SELECT * FROM role_config
+ WHERE forum = ? AND role = ?;
+ """, (fid,role)).fetchone()
+ fid = db.execute("""
+ SELECT * FROM forums WHERE id = ?
+ """,(fid,)).fetchone()['parent']
+ if the == None:
+ if role == "other":
+ raise(RuntimeError(
+ "unable to find permissions for role 'other', " +
+ "which should have associated permissions in all contexts."))
+ else:
+ return get_role_config(forum_id, "other")
+ return the
+
+def get_user_role(forum_id, username):
+ db = get_db()
+ user = db.execute('SELECT * FROM users WHERE username = ?',
+ (username,)).fetchone()
+ if user == None: return "other"
+ if user['admin']: return "bureaucrat"
+
+ fid = forum_id
+ the = None
+ while fid != None:
+ r = db.execute("""
+ SELECT * FROM role_assignments
+ WHERE forum = ? AND user = ?;
+ """,(fid,username)).fetchone()
+ # the user's role is equal to the role assignnment of the closest
+ # ancestor unless the user's role is "bureaucrat" in any ancestor
+ # in which case, the users role is "bureaucrat"
+ if the == None or (r and r['role'] == "bureaucrat"):
+ the = r
+ fid = db.execute("""
+ SELECT * FROM forums WHERE id = ?
+ """,(fid,)).fetchone()['parent']
+ return the['role'] if the != None else 'other'
+
+def get_forum_roles(forum_id):
+ db = get_db()
+
+ ancestors = db.execute("""
+ WITH RECURSIVE fs AS
+ (SELECT * FROM forums WHERE id = ?
+ UNION ALL
+ SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id)
+ SELECT * FROM fs;
+ """,(forum_id,)).fetchall()
+ configs = []
+ for a in ancestors:
+ configs += db.execute("""
+ SELECT * FROM role_config WHERE forum = ?
+ """,(a['id'],)).fetchall()
+ return set(r['role'] for r in configs)
+
+def has_permission(forum_id, username, permission, login_required=True):
+ db = get_db()
+ forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum_id,)).fetchone()
+ user = db.execute('SELECT * FROM users WHERE username = ?',
+ (username,)).fetchone() if username else None
+
+ if forum['unlisted'] and not (user and user['admin']): return False
+ if username == None and login_required: return False
+
+ role = get_user_role(forum_id, username) if username else "other"
+ if role == "bureaucrat": return True
+ config = get_role_config(forum_id, role)
+ return config[permission]
+
+def is_bureaucrat(forum_id, user):
+ if user == None: return False
+ return get_user_role(forum_id, user) == "bureaucrat"
diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index 1f089a0..280749b 100644
--- a/apioforum/static/style.css
+++ b/apioforum/static/style.css
@@ -17,10 +17,10 @@ body { font-family: sans-serif; word-wrap: break-word; }
.post:last-of-type { border-bottom: 1px solid black; }
.post-heading,.post-footer { font-size: smaller; }
-.post-heading,.post-footer,a.username {
+.post-heading,.post-footer,.username {
color: hsl(0,0%,25%);
}
-a.username {
+.username {
font-weight: bold;
text-decoration: underline;
}
@@ -36,6 +36,27 @@ a.username {
.post-anchor-link { color: hsl(0,0%,25%); }
+.deleted-post {
+ color:white;
+ background-color: hsl(0,0%,15%) !important;
+ border-left: 1px solid darkgray;
+ border-right: 1px solid darkgray;
+ border-top: 1px solid darkgray;
+}
+.deleted-post > .post-heading > * {
+ color: hsl(0,0%,85%);
+}
+.deleted-post > .post-heading > .post-heading-b > .post-anchor-link {
+ color: hsl(0,0%,60%);
+}
+.deleted-post > .post-heading > .post-heading-a > .username {
+ color: hsl(0,0%,80%);
+}
+
+.deleted-post > .post-footer {
+ color: hsl(0,0%,80%);
+}
+
.thread-top-bar, .user-top-bar {
margin-bottom: 4px;
}
@@ -80,9 +101,16 @@ dt { font-weight: bold }
img { max-width: 100% }
-.avail_tags { margin-top: 30px }
-
-nav#navbar { float: right; padding: 5px; margin: 2px; border: 1px solid black; display:flex; align-items: center; flex-wrap: wrap }
+nav#navbar {
+ float: right;
+ padding: 5px;
+ margin: 2px;
+ margin-bottom: 20px;
+ border: 1px solid black;
+ display:flex;
+ align-items: center;
+ flex-wrap: wrap;
+}
nav#navbar p { margin-left: 15px; margin-top: 0; margin-bottom: 0; margin-right: 10px; padding: 0 }
nav#navbar p:first-of-type { margin-left:0.5em }
nav#navbar a { color: blue; text-decoration: none }
@@ -168,9 +196,12 @@ nav#navbar .links { display: flex; }
content: "]";
color: grey;
}
-.actionbutton { color:blue }
+.actionbutton {
+ color:blue;
+ white-space: nowrap;
+}
-.new-post-box {
+.new-post-box, .forum-desc-box {
height:20em;
resize:vertical;
width:100%;
@@ -178,6 +209,16 @@ nav#navbar .links { display: flex; }
margin-top: 5px;
}
+#polloptions {
+ display: block;
+ resize: vertical;
+ border:1px solid black;
+ margin-top: 5px;
+ height: 5em;
+ width: 100%;
+ font-family: sans-serif;
+}
+
main {
max-width: 60ch;
margin: auto;
@@ -189,6 +230,12 @@ blockquote {
border-left: 3px solid grey;
}
+label { user-select: none; }
+
+fieldset { margin-bottom: 15px; }
+
+.warning { color: red; font-weight: bold }
+
.inline-form {
display: inline-block;
}
@@ -197,6 +244,7 @@ blockquote {
font-size: .75rem;
padding: 1px 3px;
border: 1px solid black;
+ white-space: nowrap;
}
.md table {
@@ -208,6 +256,10 @@ blockquote {
padding: 4px;
}
+.role-input, .name-input { width: 12ch; }
+
+.thing-id { color: darkgray; font-size: smaller; font-weight: normal; }
+
.breadcrumbs {
list-style: none;
}
diff --git a/apioforum/templates/common.html b/apioforum/templates/common.html
index dfbd934..f6b6f29 100644
--- a/apioforum/templates/common.html
+++ b/apioforum/templates/common.html
@@ -6,33 +6,58 @@
{{url_for('thread.view_thread', thread_id=post.thread)}}#post_{{post.id}}
{%- endmacro %}
-{% macro disp_post(post, buttons=False, footer=None) %}
-<div class="post" id="post_{{post.id}}">
+{% macro disp_post(post, buttons=False, forum=None, footer=None) %}
+<div class="post {% if post.deleted %}deleted-post{% endif %}" id="post_{{post.id}}">
<div class="post-heading">
<span class="post-heading-a">
- {{disp_user(post.author)}}
+ {% if not post.deleted %}
+ {{disp_user(post.author)}}
+ {% else %}
+ <span class="username">big brother</span>
+ {% endif %}
+
+ {% if forum != None %}
+ {% set role = get_user_role(forum, post.author) %}
+ {% if post.deleted %}
+ <span class="user-role">
+ (bureaucrat)
+ </span>
+ {% elif role != "other" %}
+ <span class="user-role">
+ ({{ role }})
+ </span>
+ {% endif %}
+ {% endif %}
+
{{ts(post.created)}}
+
{% if post.edited %}
(edited {{ts(post.updated)}})
{% endif %}
</span>
<span class="post-heading-b">
- {% if buttons %}
- {% if post.author == g.user %}
- <a class="actionbutton"
- href="{{url_for('thread.edit_post',post_id=post.id)}}">edit</a>
- <a class="actionbutton"
- href="{{url_for('thread.delete_post',post_id=post.id)}}">delete</a>
- {% endif %}
+ {% if buttons and not post.deleted %}
+ {% if post.author == g.user %}
+ <a class="actionbutton"
+ href="{{url_for('thread.edit_post',post_id=post.id)}}">edit</a>
+ {% endif %}
+ {% if post.author == g.user or (forum and has_permission(forum, g.user, "p_delete_posts")) %}
+ <a class="actionbutton"
+ href="{{url_for('thread.delete_post',post_id=post.id)}}">delete</a>
+ {% endif %}
<a class="actionbutton"
href="{{url_for('thread.view_post',post_id=post.id)}}">src</a>
{% endif %}
- <a class="post-anchor-link" href="{{post_url(post)}}">#{{post.id}}</a>
+ <a class="post-anchor-link" href="{{post_url(post)}}">#{{post.id}}</a>
</span>
</div>
<div class="post-content md">
- {{ post.content|md|safe }}
+ {% if not post.deleted %}
+ {{ post.content|md|safe }}
+ {% else %}
+ this post never existed.
+ {% endif %}
</div>
{% if footer %}
<div class="post-footer">
@@ -50,6 +75,10 @@
<span class="tag" style="color: {{the_tag.text_colour}}; background-color: {{the_tag.bg_colour}}">{{the_tag.name}}</span>
{%- endmacro %}
+{% macro ab(name,href) -%}
+<a class="actionbutton" href="{{href}}">{{name}}</a>
+{%- endmacro %}
+
{% macro breadcrumb() %}
<nav aria-label="Breadcrumb">
<ol class="breadcrumbs">
diff --git a/apioforum/templates/config_thread.html b/apioforum/templates/config_thread.html
index 2c9804e..0795ccc 100644
--- a/apioforum/templates/config_thread.html
+++ b/apioforum/templates/config_thread.html
@@ -29,6 +29,7 @@
<a href="{{url_for('thread.view_thread',thread_id=thread.id)}}">cancel</a>
</form>
+{% if has_permission(thread.forum, g.user, "p_create_polls") %}
{% if thread.poll is none %}
<h2>create poll</h2>
<form method="post" action="{{url_for('thread.create_poll',thread_id=thread.id)}}">
@@ -37,7 +38,7 @@
<label for="polltitle">question title</label>
<input type="title" id="polltitle" name="polltitle">
<br>
- <label for="polloptions">potential options (one per line)</label>
+ <label for="polloptions">options (one per line)</label>
<textarea name="polloptions" id="polloptions"></textarea>
</fieldset>
<p>important: once a poll is created, you will not be able to modify it except to delete it entirely</p>
@@ -49,7 +50,7 @@
<form action="{{url_for('thread.delete_poll',thread_id=thread.id)}}" method="post">
<input type="submit" value="confirm: delete poll">
</form>
-
+{% endif %}
{% endif %}
{% endblock %}
diff --git a/apioforum/templates/delete_thread.html b/apioforum/templates/delete_thread.html
new file mode 100644
index 0000000..aaf1de3
--- /dev/null
+++ b/apioforum/templates/delete_thread.html
@@ -0,0 +1,18 @@
+{% from 'common.html' import ts %}
+{% extends 'base.html' %}
+{% block header %}
+<h1>{% block title %}delete thread '{{thread.title}}'{% endblock %}</h1>
+{% endblock %}
+
+{% block content %}
+
+<form method="post">
+<p>deleting thread created {{ts(thread.created)}} ago with {{post_count}} posts</p>
+{% if post_count > 50 %}
+<p class="warning">thread contains more than 50 posts!</p>
+{% endif %}
+<p>confirm delete?</p>
+<input type="submit" value="delete">
+<a href="{{url_for('thread.view_thread',thread_id=thread.id)}}">cancel</a>
+</form>
+{% endblock %}
diff --git a/apioforum/templates/edit_forum.html b/apioforum/templates/edit_forum.html
new file mode 100644
index 0000000..f165676
--- /dev/null
+++ b/apioforum/templates/edit_forum.html
@@ -0,0 +1,25 @@
+{% extends 'base.html' %}
+{% block header %}
+<h1>{% block title %}{%if create %}create{% else %}edit{%endif%} forum{% endblock %}</h1>
+{% endblock %}
+
+{% block content %}
+<form method="POST">
+ <label for="name">forum name</label>
+ <input name="name" id="name" value="{{name}}" placeholder="apioforum" required maxlength="100"/>
+ <br>
+ <label for="description">forum description (markdown enabled)</label>
+ <textarea
+ name="description"
+ id="description"
+ class="forum-desc-box"
+ placeholder="this is a forum for discussing bees"
+ maxlength="6000"
+ required
+ >{{description}}</textarea>
+ <p>
+ <input type="submit" value="confirm">
+ <a href="{{cancel_link}}">cancel</a>
+ </p>
+</form>
+{% endblock %}
diff --git a/apioforum/templates/edit_permissions.html b/apioforum/templates/edit_permissions.html
new file mode 100644
index 0000000..59c9093
--- /dev/null
+++ b/apioforum/templates/edit_permissions.html
@@ -0,0 +1,94 @@
+{% extends 'base.html' %}
+{% block header %}<h1>{% block title %}role permissions for '{{forum.name}}'{% endblock %}</h1>{% endblock %}
+{% block content %}
+<p>
+ each user has a role in the forum.
+ a user may be assigned a role in the forum.
+ otherwise, the user's role is the same as the parent forum.
+ everyone's role is "other" by default.
+</p>
+<p>
+ here, a set of permissions may be associated with any role.
+ if a role does not have any permissions configured for this forum,
+ the permissions set for the role in closest ancestor forum are used.
+</p>
+<form method="post" id="role_config">
+
+{% for role_config in role_configs %}
+ <fieldset>
+ <legend id="config_{{role_config.role}}">{{role_config.role}}</legend>
+ {% macro perm(p, description, tooltip) %}
+ <input
+ type="checkbox"
+ id="perm_{{role_config.role}}_{{p}}"
+ name="perm_{{role_config.role}}_{{p}}"
+ {% if role_config[p] %}checked{% endif %}
+ />
+ <label for="perm_{{role_config.role}}_{{p}}" title="{{tooltip}}">
+ {{- description -}}
+ </label>
+ <br/>
+ {% endmacro %}
+ {{perm("p_view_forum","view the forum",
+ "allow users with the role to see the forum in listings and view information about it")}}
+ {{perm("p_create_threads","create threads",
+ "allow users with the role to create a thread in the forum")}}
+ {{perm("p_reply_threads","reply to threads",
+ "allow users with the role to create a post within a thread")}}
+ {{perm("p_view_threads","view threads",
+ "allow users with the role to view threads in the forum")}}
+ {{perm("p_manage_threads","configure others' threads",
+ "allow users with the role to modify the title/tags for others' threads or lock it to prevent new posts")}}
+ {{perm("p_delete_posts","delete others' posts and threads",
+ "allow users with the role to delete others' posts and threads")}}
+ {{perm("p_create_polls","create polls",
+ "allow users with the role to add a poll to a thread")}}
+ {{perm("p_vote","vote",
+ "allow users with the role to vote in polls")}}
+ {{perm("p_create_subforum","create subforæ",
+ "allow users with the role to create subforæ in this forum. " +
+ "they will automatically become a bureaucrat in this subforum.")}}
+ {% if role_config.role != "other" %}
+ {{perm("p_approve","approve others",
+ "allow users with the role to assign the 'approved' role to those with the 'other' role")}}
+ {% endif %}
+ <input type="hidden" name="roleconfig_{{role_config.role}}" value="present"/>
+
+ {% if forum.id != 1 or role_config.role != "other" %}
+ <hr/>
+ <input type="checkbox" name="delete_{{role_config.role}}" id="delete_{{role_config.role}}"/>
+ <label for="delete_{{role_config.role}}">remove</label>
+ {% endif %}
+ </fieldset>
+{% endfor %}
+{% if role_configs %}
+ <p>confirm changes?</p>
+ <p>
+ <input type="submit" value="confirm">
+ <a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
+ </p>
+{% endif %}
+</form>
+
+
+<fieldset>
+ <legend>add role</legend>
+ <ul>
+ {% for role in other_roles %}
+ <li>{{role}}
+ <form action="{{url_for('forum.add_role',forum_id=forum.id)}}" method="POST" style="display:inline">
+ <input type="hidden" value="{{role}}" name="role" />
+ <input type="submit" value="add" />
+ </form>
+ </li>
+ {% endfor %}
+ <li>
+ <form action="{{url_for('forum.add_role',forum_id=forum.id)}}" method="POST" style="display:inline">
+ <input type="text" name="role" class="role-input" placeholder="role name" maxlength="32"/>
+ <input type="submit" value="add" />
+ </form>
+ </li>
+ </ul>
+</fieldset>
+
+{% endblock %}
diff --git a/apioforum/templates/role_assignment.html b/apioforum/templates/role_assignment.html
new file mode 100644
index 0000000..8309506
--- /dev/null
+++ b/apioforum/templates/role_assignment.html
@@ -0,0 +1,56 @@
+{% extends 'base.html' %}
+{% from 'common.html' import ab %}
+{% block header %}<h1>{% block title %}configure user role in '{{forum.name}}'{% endblock %}</h1>{% endblock %}
+{% block content %}
+<p>
+ each user has a role in the forum.
+ here, a user may be assigned a role in the forum.
+ otherwise, the user's role is the same as the parent forum.
+ everyone's role is "other" by default.
+</p>
+{% if not is_bureaucrat(forum.id, g.user) %}
+ <p>
+ you are only allowed to approve members in this forum.
+ </p>
+{% endif %}
+
+{# <p>{{ab("role assignment list",url_for("forum.role_list_select",forum_id=forum.id))}}</p> #}
+
+<form method="post" action="{{url_for('forum.view_user_role',forum_id=forum.id)}}">
+ <label for="user">role settings for user: </label>
+ <input type="text" class="name-input" id="user" name="user" value="{{user}}"/>
+ <input type="submit" value="view"/>
+</form>
+
+{% set can_change = not invalid_user and user %}
+{% if invalid_user %}
+ <p>requested user does not exist.</p>
+{% elif user %}
+<hr/>
+<form method="post" id="role-form">
+ <p>{{user}}'s role in this forum is "{{role}}"</p>
+ {% set can_change = role == "other" or is_bureaucrat(forum.id, g.user) %}
+ {% if can_change %}
+ <label for="role">assigned role: </label>
+ <select name="role" id="role" autocomplete="off">
+ <option value="" {% if not assigned_role %}selected{% endif %}>(no assigned role)</option>
+ {% for role in forum_roles %}
+ <option value="{{role}}"
+ {% if role == assigned_role %}selected{% endif %}>
+ {{role}}
+ </option>
+ {% endfor %}
+ </select>
+ {% else %}
+ <p>you do not have permission to change the role of this user</p>
+ {% endif %}
+</form>
+{% endif %}
+
+{% if can_change %}<p>confirm changes?</p>{% endif %}
+<p>
+{% if can_change %}<input type="submit" value="confirm" form="role-form">{% endif %}
+ <a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
+</p>
+
+{% endblock %}
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index 473753d..dec9234 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -1,25 +1,56 @@
{% extends 'base.html' %}
-{% from 'common.html' import ts, tag, disp_user, post_url, forum_breadcrumb, vote_meter %}
+{% from 'common.html' import ts, tag, disp_user, post_url, forum_breadcrumb, ab, vote_meter %}
{% block header %}
-<h1>{% block title %}{{forum.name}}{%endblock%}</h1>
+<h1>{% block title %}{{forum.name}}{% endblock %} <span class="thing-id">#{{forum.id}}</span></h1>
{% if forum.id != 1 %}
{{ forum_breadcrumb(forum) }}
{% endif %}
{%endblock%}
{%block content%}
-{% if forum.description %}
-{{forum.description|md|safe}}
-{% endif %}
-<p class="avail_tags">available tags:
-{% for the_tag in avail_tags %}
-{{tag(the_tag)}}
-{% else %}
-<em>(none available)</em>
-{% endfor %}
+{{forum.description|md|safe}}
+
+<hr/>
+<div class="forum-info">
+ {% if bureaucrats|length > 0 %}
+ <p>
+ bureaucrats in this forum:
+ {% for b in bureaucrats %}
+ {{disp_user(b)}}
+ {% endfor %}
+ </p>
+ {% endif %}
+
+ {% set role = get_user_role(forum.id, g.user) %}
+ {% if role != "other" %}
+ <p>your role in this forum: {{role}}</p>
+ {% endif %}
+
+ <p>available tags:
+ {% for the_tag in avail_tags %}
+ {{tag(the_tag)}}
+ {% else %}
+ <em>(none available)</em>
+ {% endfor %}
+ </p>
+</div>
+
+<p>
+ {% if is_bureaucrat(forum.id, g.user) %}
+ {{ab("forum settings",url_for('forum.edit_forum',forum_id=forum.id))}}
+ {{ab("role/permission settings",url_for('forum.edit_roles',forum_id=forum.id))}}
+ {{ab("assign roles",url_for('forum.view_user_role',forum_id=forum.id))}}
+ {% endif %}
+ {% if has_permission(forum.id, g.user, "p_create_subforum") %}
+ {{ab("create subforum",url_for('forum.create_forum',forum_id=forum.id))}}
+ {% endif %}
+ {% if not is_bureaucrat(forum.id, g.user) and has_permission(forum.id, g.user, "p_approve") %}
+ {{ab("approve users",url_for('forum.view_user_role',forum_id=forum.id))}}
+ {% endif %}
+</p>
{% if subforums %}
-<h2>subforae</h2>
+<h2>subforæ</h2>
<div class="forum-list">
{% for subforum in subforums %}
<div class="listing">
@@ -44,12 +75,15 @@
<h2>threads</h2>
<p>
-{% if g.user %}
+{% if has_permission(forum.id, g.user, "p_create_threads") %}
<a class="actionbutton" href="{{url_for('forum.create_thread',forum_id=forum.id)}}">create new thread</a>
-{% else %}
+{% elif has_permission(forum.id, g.user, "p_create_threads", login_required=False) %}
please log in to create a new thread
+{% else %}
+you do not have permission to create threads in this forum
{% endif %}
-</p>
+
+{% if has_permission(forum.id, g.user, "p_view_threads", login_required=False) %}
<div class="thread-list">
{%for thread in threads%}
<div class="listing">
@@ -71,7 +105,7 @@ please log in to create a new thread
{{ ts(thread.created) }}
</div>
</div>
- {#{% if thread.mrp_id %}#}
+ {% if not thread.mrp_deleted %}
<div class="listing-caption">
{{ disp_user(thread.mrp_author) }}
<span class="thread-preview-ts">
@@ -83,7 +117,14 @@ please log in to create a new thread
</a>
</span>
</div>
- {#{% endif %}#}
+ {% else %}
+ <div class="listing-caption">
+ <a class="thread-preview-post"
+ href="{{url_for('thread.view_thread',thread_id=thread.id)}}#post_{{thread.mrp_id}}">
+ latest post
+ </a>
+ </div>
+ {% endif %}
{% if thread_polls[thread.id] %}
<div class="thread-vote-summary">
{{ vote_meter(thread_polls[thread.id]) }}
@@ -105,6 +146,7 @@ please log in to create a new thread
</nav>
-</nav>
-</main>
+{% else %}
+<p>you do not have permission to view threads in this forum</p>
+{% endif %}
{%endblock%}
diff --git a/apioforum/templates/view_thread.html b/apioforum/templates/view_thread.html
index 29914e8..132fd44 100644
--- a/apioforum/templates/view_thread.html
+++ b/apioforum/templates/view_thread.html
@@ -1,7 +1,7 @@
{% from 'common.html' import disp_post,tag,thread_breadcrumb,vote_meter %}
{% extends 'base.html' %}
{% block header %}
-<h1>{%block title %}{{thread.title}}{% endblock %}</h1>
+<h1>{%block title %}{{thread.title}}{% endblock %} <span class="thing-id">#{{thread.id}}</span></h1>
{{ thread_breadcrumb(thread) }}
{% endblock %}
@@ -17,9 +17,12 @@
{% endif %}
<div class="thread-top-bar">
<span class="thread-top-bar-a">
- {% if g.user == thread.creator %}
+ {% if g.user == thread.creator or has_permission(thread.forum, g.user, "p_manage_threads") %}
<a class="actionbutton" href="{{url_for('thread.config_thread',thread_id=thread.id)}}">configure thread</a>
{% endif %}
+ {% if has_permission(thread.forum, g.user, "p_delete_posts") %}
+ <a class="actionbutton" href="{{url_for('thread.delete_thread',thread_id=thread.id)}}">delete thread</a>
+ {% endif %}
</span>
&nbsp;
<span class="thread-top-bar-b">
@@ -39,29 +42,37 @@
{# this is bad but it's going to get refactored anyway #}
{% set footer %}
{% if vote.is_retraction %}
- {{post.author}} retracted their vote
+ {% if not post.deleted %}
+ {{post.author}} retracted their vote
+ {% else %}
+ this post retracted a vote
+ {% endif %}
{% else %}
{% set option = poll.options[option_idx-1] %}
{% if vote.current %}
{{post.author}} votes for {{option_idx}}: {{option.text}}
{% else %}
- {{post.author}} voted for {{option_idx}}: {{option.text}}, but later changed their vote
+ {% if not post.deleted %}
+ {{post.author}} voted for {{option_idx}}: {{option.text}}, but later changed their vote
+ {% else %}
+ this post presented a vote that was later changed
+ {% endif %}
{% endif %}
{% endif %}
{% endset %}
- {{ disp_post(post, buttons=True, footer=footer) }}
+ {{ disp_post(post, forum=thread.forum, buttons=True, footer=footer) }}
{% else %}
- {{ disp_post(post, buttons=True) }}
+ {{ disp_post(post, forum=thread.forum, buttons=True) }}
{% endif %}
{% endfor %}
</div>
-{% if g.user %}
+{% if g.user and has_permission(thread.forum, g.user, "p_reply_threads") %}
<form class="new-post" action="{{url_for('thread.create_post',thread_id=thread.id)}}" method="POST">
<textarea class="new-post-box" placeholder="your post here..." name="content"></textarea>
- {% if poll %}
+ {% if poll and has_permission(thread.forum, g.user, "p_vote") %}
<fieldset>
<legend>poll: {{poll.title}}</legend>
<p>if you want, you can submit a vote along with this post. if you have previously voted
@@ -73,18 +84,20 @@
{% if has_voted %}
<br>
<input type="radio" id="retractvote" name="poll" value="retractvote">
- <label for="retractvote">retract my vote, and go back to having no vote on this poll</label>
+ <label for="retractvote">clear current vote</label>
{% endif %}
{% for opt in poll.options %}
<br>
<input type="radio" id="option_{{opt.option_idx}}" name="poll" value="{{opt.option_idx}}">
- <label for="option_{{opt.option_idx}}">submit a vote for #{{opt.option_idx}} - {{opt.text}}</label>
+ <label for="option_{{opt.option_idx}}">#{{opt.option_idx}} - {{opt.text}}</label>
{% endfor %}
</fieldset>
{% endif %}
<input type="submit" value="yes">
</form>
+{% elif g.user %}
+<p>you do not have permission to reply to this thread</p>
{% else %}
<p>please log in to reply to this thread</p>
{% endif %}
diff --git a/apioforum/templates/view_unlisted.html b/apioforum/templates/view_unlisted.html
new file mode 100644
index 0000000..c0fd074
--- /dev/null
+++ b/apioforum/templates/view_unlisted.html
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% from 'common.html' import forum_breadcrumb %}
+{% block header %}
+<h1>{% block title %}unlisted subforæ in '{{forum.name}}'{% endblock %}</h1>
+{% if forum.id != 1 %}
+ {{ forum_breadcrumb(forum) }}
+{% endif %}
+{% endblock %}
+
+{% block content %}
+<form method="POST">
+ {% if unlisted %}
+ <ul>
+ {% for f in unlisted %}
+ <li>
+ <a href="{{url_for('forum.view_forum',forum_id=f.id)}}">{{f.name}}</a>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>there are no unlisted subforæ in '{{forum.name}}'</p>
+ {% endif %}
+</form>
+{% endblock %}
diff --git a/apioforum/thread.py b/apioforum/thread.py
index 415a45c..b281d0a 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -7,6 +7,7 @@ from flask import (
url_for, flash, jsonify
)
from .db import get_db
+from .roles import has_permission
from .forum import get_avail_tags
bp = Blueprint("thread", __name__, url_prefix="/thread")
@@ -42,62 +43,63 @@ def view_thread(thread_id,page=1):
thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone()
if thread is None:
abort(404)
- else:
- posts = db.execute("""
- SELECT * FROM posts
- WHERE posts.thread = ?
- ORDER BY created ASC
- LIMIT ? OFFSET ?;
- """,(
- thread_id,
- POSTS_PER_PAGE,
- (page-1)*POSTS_PER_PAGE,
- )).fetchall()
- tags = db.execute(
- """SELECT tags.* FROM tags
- INNER JOIN thread_tags ON thread_tags.tag = tags.id
- WHERE thread_tags.thread = ?
- ORDER BY tags.id""",(thread_id,)).fetchall()
- poll = None
- votes = None
- if thread['poll'] is not None:
- poll_row= db.execute("""
- SELECT polls.*,total_vote_counts.total_votes FROM polls
- LEFT OUTER JOIN total_vote_counts ON polls.id = total_vote_counts.poll
- WHERE polls.id = ?;
- """,(thread['poll'],)).fetchone()
- options = db.execute("""
- SELECT poll_options.*, vote_counts.num
- FROM poll_options
- LEFT OUTER JOIN vote_counts ON poll_options.poll = vote_counts.poll
- AND poll_options.option_idx = vote_counts.option_idx
- WHERE poll_options.poll = ?
- ORDER BY option_idx asc;
- """,(poll_row['id'],)).fetchall()
- poll = {}
- poll.update(poll_row)
- poll['options'] = options
- votes = {}
- # todo: optimise this somehow
- for post in posts:
- if post['vote'] is not None:
- votes[post['id']] = db.execute("SELECT * FROM votes WHERE id = ?",(post['vote'],)).fetchone()
+ if not has_permission(thread['forum'], g.user, "p_view_threads", False):
+ abort(403)
+ posts = db.execute("""
+ SELECT * FROM posts
+ WHERE posts.thread = ?
+ ORDER BY created ASC
+ LIMIT ? OFFSET ?;
+ """,(
+ thread_id,
+ POSTS_PER_PAGE,
+ (page-1)*POSTS_PER_PAGE,
+ )).fetchall()
+ tags = db.execute(
+ """SELECT tags.* FROM tags
+ INNER JOIN thread_tags ON thread_tags.tag = tags.id
+ WHERE thread_tags.thread = ?
+ ORDER BY tags.id""",(thread_id,)).fetchall()
+ poll = None
+ votes = None
+ if thread['poll'] is not None:
+ poll_row= db.execute("""
+ SELECT polls.*,total_vote_counts.total_votes FROM polls
+ LEFT OUTER JOIN total_vote_counts ON polls.id = total_vote_counts.poll
+ WHERE polls.id = ?;
+ """,(thread['poll'],)).fetchone()
+ options = db.execute("""
+ SELECT poll_options.*, vote_counts.num
+ FROM poll_options
+ LEFT OUTER JOIN vote_counts ON poll_options.poll = vote_counts.poll
+ AND poll_options.option_idx = vote_counts.option_idx
+ WHERE poll_options.poll = ?
+ ORDER BY option_idx asc;
+ """,(poll_row['id'],)).fetchall()
+ poll = {}
+ poll.update(poll_row)
+ poll['options'] = options
+ votes = {}
+ # todo: optimise this somehow
+ for post in posts:
+ if post['vote'] is not None:
+ votes[post['id']] = db.execute("SELECT * FROM votes WHERE id = ?",(post['vote'],)).fetchone()
- if g.user is None or poll is None:
- has_voted = None
- else:
- v = db.execute("SELECT * FROM votes WHERE poll = ? AND user = ? AND current AND NOT is_retraction;",(poll['id'],g.user)).fetchone()
- has_voted = v is not None
-
- return render_template(
- "view_thread.html",
- posts=posts,
- thread=thread,
- tags=tags,
- poll=poll,
- votes=votes,
- has_voted=has_voted,
- )
+ if g.user is None or poll is None:
+ has_voted = None
+ else:
+ v = db.execute("SELECT * FROM votes WHERE poll = ? AND user = ? AND current AND NOT is_retraction;",(poll['id'],g.user)).fetchone()
+ has_voted = v is not None
+
+ return render_template(
+ "view_thread.html",
+ posts=posts,
+ thread=thread,
+ tags=tags,
+ poll=poll,
+ votes=votes,
+ has_voted=has_voted,
+ )
def register_vote(thread,pollval):
if pollval is None or pollval == 'dontvote':
@@ -140,7 +142,8 @@ def create_poll(thread_id):
err = "that thread does not exist"
elif g.user is None:
err = "you need to be logged in to do that"
- elif g.user != thread['creator']:
+ elif g.user != thread['creator'] and \
+ not has_permission(thread['forum'],g.user,"p_manage_threads"):
err = "you can only create polls on threads that you own"
elif thread['poll'] is not None:
err = "a poll already exists for that thread"
@@ -148,6 +151,8 @@ def create_poll(thread_id):
err = "poll title can't be empty"
elif len(polloptions) < 2:
err = "you must provide at least 2 options"
+ elif not has_permission(thread['forum'], g.user, "p_create_polls"):
+ err = "you do not have permission to do that"
if err is not None:
flash(err)
@@ -177,7 +182,8 @@ def delete_poll(thread_id):
err = "that thread does not exist"
elif g.user is None:
err = "you need to be logged in to do that"
- elif g.user != thread['creator']:
+ elif g.user != thread['creator'] and not \
+ has_permission(thread['forum'], g.user, "p_manage_threads"):
err = "you can only delete polls on threads that you own"
elif thread['poll'] is None:
err = "there is no poll to delete on this thread"
@@ -187,7 +193,6 @@ def delete_poll(thread_id):
return fail
else:
pollid = thread['poll']
-
db.execute("UPDATE posts SET vote = NULL WHERE thread = ?",(thread_id,)) # this assumes only max one poll per thread
db.execute("DELETE FROM votes WHERE poll = ?",(pollid,))
db.execute("DELETE FROM poll_options WHERE poll = ?",(pollid,))
@@ -201,60 +206,89 @@ def delete_poll(thread_id):
def create_post(thread_id):
if g.user is None:
flash("you need to log in before you can post")
- return redirect(url_for('thread.view_thread',thread_id=thread_id))
+ db = get_db()
+ content = request.form['content']
+ thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone()
+ if len(content.strip()) == 0:
+ flash("you cannot post an empty message")
+ elif not thread:
+ flash("that thread does not exist")
+ elif not has_permission(thread['forum'], g.user, "p_reply_threads"):
+ flash("you do not have permission to do this")
+ elif not has_permission(thread['forum'], g.user, "p_view_threads"):
+ flash("you do not have permission to do this")
+ elif not has_permission(thread['forum'], g.user, "p_vote") \
+ and 'poll' in request.form:
+ flash("you do not have permission to do this")
else:
- db = get_db()
- content = request.form['content']
- thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone()
- if len(content.strip()) == 0:
- flash("you cannot post an empty message")
- elif not thread:
- flash("that thread does not exist")
- else:
- vote_id = None
- if thread['poll'] is not None:
- pollval = request.form.get('poll')
- try:
- vote_id = register_vote(thread,pollval)
- except ValueError:
- flash("invalid poll form value")
- return redirect(url_for('thread.view_thread',thread_id=thread_id))
+ vote_id = None
+ if thread['poll'] is not None:
+ pollval = request.form.get('poll')
+ try:
+ vote_id = register_vote(thread,pollval)
+ except ValueError:
+ flash("invalid poll form value")
+ return redirect(url_for('thread.view_thread',thread_id=thread_id))
- cur = db.cursor()
- cur.execute("""
- INSERT INTO posts (thread,author,content,created,vote)
- VALUES (?,?,?,current_timestamp,?);
- """,(thread_id,g.user,content,vote_id))
- post_id = cur.lastrowid
- cur.execute(
- "UPDATE threads SET updated = current_timestamp WHERE id = ?;",
- (thread_id,)
- )
- db.commit()
- flash("post posted postfully")
- return redirect(post_jump(thread_id, post_id))
+ cur = db.cursor()
+ cur.execute("""
+ INSERT INTO posts (thread,author,content,created,vote)
+ VALUES (?,?,?,current_timestamp,?);
+ """,(thread_id,g.user,content,vote_id))
+ post_id = cur.lastrowid
+ cur.execute(
+ "UPDATE threads SET updated = current_timestamp WHERE id = ?;",
+ (thread_id,)
+ )
+ db.commit()
+ flash("post posted postfully")
+ return redirect(post_jump(thread_id, post_id))
return redirect(url_for('thread.view_thread',thread_id=thread_id))
@bp.route("/delete_post/<int:post_id>", methods=["GET","POST"])
def delete_post(post_id):
db = get_db()
post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone()
+ thread = db.execute("SELECT * FROM threads WHERE id = ?",(post['thread'],)).fetchone()
if post is None:
flash("that post doesn't exist")
return redirect("/")
- if post['author'] != g.user:
- flash("you can only delete posts that you created")
+ if post['author'] != g.user and not has_permission(thread['forum'], g.user, "p_delete_posts"):
+ flash("you do not have permission to do that")
return redirect(url_for("thread.view_thread",thread_id=post["thread"]))
if request.method == "POST":
- # todo: don't actually delete, just mark as deleted or something (and wipe content)
- # so that you can have a "this post was deleted" thing
- db.execute("DELETE FROM posts WHERE id = ?",(post_id,))
+ db.execute("""
+ UPDATE posts SET
+ content = '',
+ deleted = 1
+ WHERE id = ?""",(post_id,))
db.commit()
flash("post deleted deletedly")
return redirect(url_for("thread.view_thread",thread_id=post["thread"]))
else:
return render_template("delete_post.html",post=post)
+@bp.route("/delete_thread/<int:thread_id>", methods=["GET","POST"])
+def delete_thread(thread_id):
+ db = get_db()
+ thread = db.execute("SELECT * FROM threads WHERE id = ?",(thread_id,)).fetchone()
+ if thread is None:
+ flash("that thread doesn't exist")
+ return redirect("/")
+ if not has_permission(thread['forum'], g.user, "p_delete_posts"):
+ flash("you do not have permission to do that")
+ return redirect(url_for("thread.view_thread",thread_id=thread_id))
+ if request.method == "POST":
+ db.execute("DELETE FROM posts WHERE thread = ?",(thread_id,))
+ db.execute("DELETE FROM threads WHERE id = ?",(thread_id,))
+ db.commit()
+ flash("thread deleted deletedly")
+ return redirect(url_for("forum.view_forum",forum_id=thread['forum']))
+ else:
+ count = db.execute("SELECT num_replies FROM number_of_posts WHERE thread = ?",
+ (thread_id,)).fetchone()[0]
+ return render_template("delete_thread.html",thread=thread,post_count=count)
+
@bp.route("/edit_post/<int:post_id>",methods=["GET","POST"])
def edit_post(post_id):
@@ -307,7 +341,9 @@ def config_thread(thread_id):
err = None
if g.user is None:
err = "you need to be logged in to do that"
- elif g.user != thread['creator']:
+ elif not has_permission(thread['forum'], g.user, "p_view_threads"):
+ err = "you do not have permission to do that"
+ elif g.user != thread['creator'] and not has_permission(thread['forum'], g.user, "p_manage_threads"):
err = "you can only configure threads that you own"
if err is not None:
diff --git a/apioforum/user.py b/apioforum/user.py
index 9f4bc5b..bbdd060 100644
--- a/apioforum/user.py
+++ b/apioforum/user.py
@@ -16,8 +16,11 @@ def view_user(username):
user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone()
if user is None:
abort(404)
- posts = db.execute(
- "SELECT * FROM posts WHERE author = ? ORDER BY created DESC LIMIT 25;",(username,)).fetchall()
+ posts = db.execute("""
+ SELECT * FROM posts
+ WHERE author = ? AND deleted = 0
+ ORDER BY created DESC
+ LIMIT 25;""",(username,)).fetchall()
return render_template("view_user.html", user=user, posts=posts)
@bp.route("/<username>/edit", methods=["GET","POST"])