diff options
author | ubq323 <ubq323> | 2021-08-18 23:54:49 +0000 |
---|---|---|
committer | ubq323 <ubq323> | 2021-08-18 23:54:49 +0000 |
commit | d2e51e7b8bb2f765e79c98f13165b0faf59a302c (patch) | |
tree | 8b5972c5e05d44cfabca011fab50399c04ca5456 | |
parent | c51c87b6d012c2a250054b47e330e1c504aebb4a (diff) | |
parent | 17357a3fb5cf603ff79daad644f4a4c0fbe42150 (diff) |
merge trunk into this
-rw-r--r-- | apioforum/__init__.py | 5 | ||||
-rw-r--r-- | apioforum/db.py | 36 | ||||
-rw-r--r-- | apioforum/forum.py | 262 | ||||
-rw-r--r-- | apioforum/roles.py | 97 | ||||
-rw-r--r-- | apioforum/static/style.css | 66 | ||||
-rw-r--r-- | apioforum/templates/common.html | 53 | ||||
-rw-r--r-- | apioforum/templates/config_thread.html | 5 | ||||
-rw-r--r-- | apioforum/templates/delete_thread.html | 18 | ||||
-rw-r--r-- | apioforum/templates/edit_forum.html | 25 | ||||
-rw-r--r-- | apioforum/templates/edit_permissions.html | 94 | ||||
-rw-r--r-- | apioforum/templates/role_assignment.html | 56 | ||||
-rw-r--r-- | apioforum/templates/view_forum.html | 80 | ||||
-rw-r--r-- | apioforum/templates/view_thread.html | 33 | ||||
-rw-r--r-- | apioforum/templates/view_unlisted.html | 24 | ||||
-rw-r--r-- | apioforum/thread.py | 224 | ||||
-rw-r--r-- | apioforum/user.py | 7 |
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> <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"]) |