From 3a3e427695f3f6754297a2dde335358518d3373a Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Fri, 25 Jun 2021 05:33:00 +0000
Subject: permissions table

---
 apioforum/db.py                     | 19 ++++++++++++++++++-
 apioforum/templates/view_forum.html |  1 +
 2 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/apioforum/db.py b/apioforum/db.py
index 7dd635e..06682d6 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -117,7 +117,24 @@ CREATE VIEW most_recent_posts AS
 CREATE VIEW number_of_posts AS
     SELECT thread, count(*) AS num_replies FROM posts GROUP BY thread;
 """,
-    
+"""
+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_delete_threads INT NOT NULL DEFAULT 0,
+    p_lock_threads INT NOT NULL DEFAULT 0,
+    p_approve INT NOT NULL DEFAULT 0,
+    p_create_subforum INT NOT NULL DEFAULT 0
+);
+
+INSERT INTO role_config (role,forum) SELECT "approved",id FROM forums;
+INSERT INTO role_config (role,forum) SELECT "other",id FROM forums;
+""",
 ]
 
 def init_db():
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index 96c51bb..fce051f 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -10,6 +10,7 @@
 {%block content%}
 {% if forum.description %}
 {{forum.description|md|safe}}
+<hr/>
 {% endif %}
 
 {% if subforums %}
-- 
cgit v1.2.3


From eb340b12ae9844d5ff2e4a927753c4d92d3f56e0 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Tue, 29 Jun 2021 19:53:24 +0000
Subject: role config, UI, and things

---
 apioforum/db.py                           | 13 ++++-
 apioforum/forum.py                        | 23 ++++++++
 apioforum/roles.py                        | 48 ++++++++++++++++
 apioforum/static/style.css                |  4 ++
 apioforum/templates/edit_permissions.html | 94 +++++++++++++++++++++++++++++++
 apioforum/templates/view_forum.html       |  2 +-
 6 files changed, 181 insertions(+), 3 deletions(-)
 create mode 100644 apioforum/roles.py
 create mode 100644 apioforum/templates/edit_permissions.html

diff --git a/apioforum/db.py b/apioforum/db.py
index 06682d6..b5cba39 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -123,11 +123,14 @@ CREATE TABLE role_config (
     forum NOT NULL REFERENCES forums(id),
     id INTEGER PRIMARY KEY,
 
+    inherit INT NOT NULL DEFAULT 0,
+
     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_delete_threads INT NOT NULL DEFAULT 0,
-    p_lock_threads INT NOT NULL DEFAULT 0,
+    p_manage_threads 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
 );
@@ -135,6 +138,12 @@ CREATE TABLE role_config (
 INSERT INTO role_config (role,forum) SELECT "approved",id FROM forums;
 INSERT INTO role_config (role,forum) SELECT "other",id FROM forums;
 """,
+"""
+CREATE TRIGGER default_role_config AFTER INSERT ON forums BEGIN
+    INSERT INTO role_config (role,forum) VALUES ("approved",new.id);
+    INSERT INTO role_config (role,forum) VALUES ("other",new.id);
+END;
+"""
 ]
 
 def init_db():
diff --git a/apioforum/forum.py b/apioforum/forum.py
index 7d6f0f0..69d7650 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -8,6 +8,7 @@ from flask import (
 
 from .db import get_db
 from .mdrender import render
+from .roles import forum_perms, overridden_perms
 
 from sqlite3 import OperationalError
 import datetime
@@ -118,6 +119,28 @@ def create_thread(forum_id):
         
     return render_template("create_thread.html")
 
+@bp.route("/<int:forum_id>/roles",methods=("GET","POST"))
+def edit_roles(forum_id):
+    db = get_db()
+    forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum_id,)).fetchone()
+    role_configs = db.execute(
+        "SELECT * FROM role_config WHERE forum = ? ORDER BY ID ASC",
+        (forum_id,)).fetchall()
+    overridden = {}
+    for c in role_configs:
+        overridden[c['id']] = overridden_perms(forum_id,c['role'])
+
+    return render_template("edit_permissions.html",
+            forum=forum,
+            role_configs=role_configs,
+            other_roles=["the","test","placeholder"],
+            overridden=overridden
+            )
+
+@bp.route("/<int:forum_id>/roles/new/<role_name>",methods=["POST"])
+def add_role(forum_id,role_name):
+    db.execute
+
 @bp.route("/search")
 def search():
     db = get_db()
diff --git a/apioforum/roles.py b/apioforum/roles.py
new file mode 100644
index 0000000..f364b04
--- /dev/null
+++ b/apioforum/roles.py
@@ -0,0 +1,48 @@
+
+from .db import get_db
+
+permissions = [
+    "p_create_threads",
+    "p_reply_threads",
+    "p_manage_threads",
+    "p_view_threads",
+    "p_vote",
+    "p_create_polls",
+    "p_approve",
+    "p_create_subforum"
+]
+
+def get_role_config(forum_id, role):
+    db = get_db()
+    return db.execute("""
+        SELECT * FROM role_config 
+        WHERE forum = ? AND role = ?;
+        """, (forum_id,role)).fetchone()
+
+def overridden_perms(forum_id, role):
+    db = get_db()
+    p = {}
+    for perm in permissions:
+        p[perm] = False
+    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()[1:]
+    for ancestor in ancestors:
+        config = get_role_config(ancestor['id'], role)
+        if config and config['inherit']:
+            for perm in permissions:
+                p[perm] = p[perm] or not config[perm]
+    return p
+
+def forum_perms(forum_id, role):
+    role_config = get_role_config(forum_id, role)
+    if not role_config:
+        role_config = get_role_config(forum_id, "other")
+    p = {}
+    overridden = overridden_perms(forum_id, role)
+    for perm in permissions:
+        p[perm] = role_config[perm] and not overridden[perm]
diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index 4403f18..09df395 100644
--- a/apioforum/static/style.css
+++ b/apioforum/static/style.css
@@ -181,6 +181,10 @@ blockquote {
     border-left: 3px solid grey;
 }
 
+label { user-select: none; }
+
+fieldset { margin-bottom: 15px; }
+
 .search-form {
     display: inline-block;
 }
diff --git a/apioforum/templates/edit_permissions.html b/apioforum/templates/edit_permissions.html
new file mode 100644
index 0000000..a32ceda
--- /dev/null
+++ b/apioforum/templates/edit_permissions.html
@@ -0,0 +1,94 @@
+{% extends 'base.html' %}
+{% from 'common.html' import tag %}
+{% block header %}<h1>{% block title %}role permissions for '{{forum.name}}'{% endblock %}</h1>{% endblock %}
+{% block content %}
+<p>
+	each user has a role in this forum. 
+	the permissions associated with different roles can be configured here.
+</p>
+<p>
+	there are three special roles: "bureaucrat", "approved", and "other".
+	bureaucrats are automatically granted every permission.
+	everyone by default has the "other" role.
+	an assigned role is inherited by all subforæ unless overridden.
+</p>
+<p>
+	if a role's permissions are set to inherit, 
+	permissions disabled for a role are disabled for that role in all subforæ.
+</p>
+<form method="post" id="role_config">
+
+{% set show_footnote = False %}
+{% 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="{{role_config.role}}_{{p}}" 
+				name="{{role_config.role}}_{{p}}" 
+				{% if role_config[p] %}checked{% endif %}
+			/>
+			<label for="{{role_config.role}}_{{p}}" title="{{tooltip}}">
+				{{- description -}}
+				{%- if overridden[role_config.id][p] -%}
+					*
+					{%- set show_footnote = True -%}
+				{%- endif -%}
+			</label>
+			<br/>
+		{% endmacro %}
+		{{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 delete, lock, or modify the title/tags for others' threads")}}
+		{{perm("p_create_polls","create polls",
+				"allow users with the role to create poll threads")}}
+		{{perm("p_vote","vote",
+				"allow users with the role to vote on poll threads")}}
+		{{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 %}
+		<hr/>
+		<input 
+			type="checkbox" 
+			id="{{role_config.role}}_inherit"
+			name="{{role_config.role}}_inherit"
+			{% if role_config.inherit %}checked{% endif %}
+		/>
+		<label for="{{role_config.role}}_inherit">inherit</label>
+	</fieldset>
+{% endfor %}
+
+{% if show_footnote %}
+	<p>* disabled in inherited permissions from parent forum</p>
+{% endif %}
+</form>
+
+{% if other_roles %}
+	<fieldset>
+		<legend>roles from parent foræ</legend>
+		<ul>
+			{% for role in other_roles %}
+			<li>{{role}} 
+				<form action="{{url_for('forum.add_role',forum_id=forum.id,role_name=role)}}" method="POST" style="display:inline">
+					<input type="submit" value="add" />
+				</form>
+			</li>
+			{% endfor %}
+		</ul>
+	</fieldset>
+{% endif %}
+
+<p>confirm changes?</p>
+<input type="submit" value="confirm" form="role_config">
+<a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
+
+{% endblock %}
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index fce051f..d3d09e1 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -14,7 +14,7 @@
 {% endif %}
 
 {% if subforums %}
-<h2>subforae</h2>
+<h2>subforæ</h2>
 <div class="forum-list">
 	{% for subforum in subforums %}
 		<div class="listing">
-- 
cgit v1.2.3


From 338d67854d5eca63b6596fb309589755012c4ca2 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Fri, 16 Jul 2021 09:46:44 +0000
Subject: committing what I have so that ubq can work on it

---
 apioforum/db.py                           | 12 +-----
 apioforum/forum.py                        | 10 ++---
 apioforum/roles.py                        | 40 ++++-------------
 apioforum/static/style.css                |  2 +
 apioforum/templates/edit_permissions.html | 71 +++++++++++++------------------
 5 files changed, 46 insertions(+), 89 deletions(-)

diff --git a/apioforum/db.py b/apioforum/db.py
index b5cba39..5ffd5d9 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -123,8 +123,6 @@ CREATE TABLE role_config (
     forum NOT NULL REFERENCES forums(id),
     id INTEGER PRIMARY KEY,
 
-    inherit INT NOT NULL DEFAULT 0,
-
     p_create_threads INT NOT NULL DEFAULT 1,
     p_reply_threads INT NOT NULL DEFAULT 1,
     p_view_threads INT NOT NULL DEFAULT 1,
@@ -135,14 +133,8 @@ CREATE TABLE role_config (
     p_create_subforum INT NOT NULL DEFAULT 0
 );
 
-INSERT INTO role_config (role,forum) SELECT "approved",id FROM forums;
-INSERT INTO role_config (role,forum) SELECT "other",id FROM forums;
-""",
-"""
-CREATE TRIGGER default_role_config AFTER INSERT ON forums BEGIN
-    INSERT INTO role_config (role,forum) VALUES ("approved",new.id);
-    INSERT INTO role_config (role,forum) VALUES ("other",new.id);
-END;
+INSERT INTO role_config (role,forum) VALUES ("approved",1);
+INSERT INTO role_config (role,forum) VALUES ("other",1);
 """
 ]
 
diff --git a/apioforum/forum.py b/apioforum/forum.py
index 69d7650..4b7522c 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -126,20 +126,16 @@ def edit_roles(forum_id):
     role_configs = db.execute(
         "SELECT * FROM role_config WHERE forum = ? ORDER BY ID ASC",
         (forum_id,)).fetchall()
-    overridden = {}
-    for c in role_configs:
-        overridden[c['id']] = overridden_perms(forum_id,c['role'])
 
     return render_template("edit_permissions.html",
             forum=forum,
             role_configs=role_configs,
             other_roles=["the","test","placeholder"],
-            overridden=overridden
             )
 
-@bp.route("/<int:forum_id>/roles/new/<role_name>",methods=["POST"])
-def add_role(forum_id,role_name):
-    db.execute
+@bp.route("/<int:forum_id>/roles/new",methods=["POST"])
+def add_role(forum_id):
+    return "placeholder"
 
 @bp.route("/search")
 def search():
diff --git a/apioforum/roles.py b/apioforum/roles.py
index f364b04..71efcbd 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -14,35 +14,13 @@ permissions = [
 
 def get_role_config(forum_id, role):
     db = get_db()
-    return db.execute("""
-        SELECT * FROM role_config 
-        WHERE forum = ? AND role = ?;
-        """, (forum_id,role)).fetchone()
 
-def overridden_perms(forum_id, role):
-    db = get_db()
-    p = {}
-    for perm in permissions:
-        p[perm] = False
-    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()[1:]
-    for ancestor in ancestors:
-        config = get_role_config(ancestor['id'], role)
-        if config and config['inherit']:
-            for perm in permissions:
-                p[perm] = p[perm] or not config[perm]
-    return p
-
-def forum_perms(forum_id, role):
-    role_config = get_role_config(forum_id, role)
-    if not role_config:
-        role_config = get_role_config(forum_id, "other")
-    p = {}
-    overridden = overridden_perms(forum_id, role)
-    for perm in permissions:
-        p[perm] = role_config[perm] and not overridden[perm]
+    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("""
+            """).fetchone()['parent']
diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index 09df395..3813d63 100644
--- a/apioforum/static/style.css
+++ b/apioforum/static/style.css
@@ -195,6 +195,8 @@ fieldset { margin-bottom: 15px; }
     border: 1px solid black;
 }
 
+.role-input { width: 12ch; }
+
 .breadcrumbs {
     list-style: none;
 }
diff --git a/apioforum/templates/edit_permissions.html b/apioforum/templates/edit_permissions.html
index a32ceda..e79c0c7 100644
--- a/apioforum/templates/edit_permissions.html
+++ b/apioforum/templates/edit_permissions.html
@@ -3,22 +3,20 @@
 {% block header %}<h1>{% block title %}role permissions for '{{forum.name}}'{% endblock %}</h1>{% endblock %}
 {% block content %}
 <p>
-	each user has a role in this forum. 
-	the permissions associated with different roles can be configured here.
+	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>
-	there are three special roles: "bureaucrat", "approved", and "other".
-	bureaucrats are automatically granted every permission.
-	everyone by default has the "other" role.
-	an assigned role is inherited by all subforæ unless overridden.
-</p>
-<p>
-	if a role's permissions are set to inherit, 
-	permissions disabled for a role are disabled for that role in all subforæ.
+	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.
+	if no permissions are set for the role in any ancestor forum,
+	the permissions for the role "other" are used.
 </p>
 <form method="post" id="role_config">
 
-{% set show_footnote = False %}
 {% for role_config in role_configs %}
 	<fieldset>
 	<legend id="config_{{role_config.role}}">{{role_config.role}}</legend>
@@ -31,10 +29,6 @@
 			/>
 			<label for="{{role_config.role}}_{{p}}" title="{{tooltip}}">
 				{{- description -}}
-				{%- if overridden[role_config.id][p] -%}
-					*
-					{%- set show_footnote = True -%}
-				{%- endif -%}
 			</label>
 			<br/>
 		{% endmacro %}
@@ -51,41 +45,36 @@
 		{{perm("p_vote","vote",
 				"allow users with the role to vote on poll threads")}}
 		{{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.")}}
+				"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 %}
-		<hr/>
-		<input 
-			type="checkbox" 
-			id="{{role_config.role}}_inherit"
-			name="{{role_config.role}}_inherit"
-			{% if role_config.inherit %}checked{% endif %}
-		/>
-		<label for="{{role_config.role}}_inherit">inherit</label>
 	</fieldset>
 {% endfor %}
 
-{% if show_footnote %}
-	<p>* disabled in inherited permissions from parent forum</p>
-{% endif %}
 </form>
 
-{% if other_roles %}
-	<fieldset>
-		<legend>roles from parent foræ</legend>
-		<ul>
-			{% for role in other_roles %}
-			<li>{{role}} 
-				<form action="{{url_for('forum.add_role',forum_id=forum.id,role_name=role)}}" method="POST" style="display:inline">
-					<input type="submit" value="add" />
-				</form>
-			</li>
-			{% endfor %}
-		</ul>
-	</fieldset>
-{% endif %}
+<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,role_name=role)}}" method="POST" style="display:inline">
+				<input type="text" name="role" class="role-input" placeholder="role name"/>
+				<input type="submit" value="add" />
+			</form>
+		</li>
+	</ul>
+</fieldset>
 
 <p>confirm changes?</p>
 <input type="submit" value="confirm" form="role_config">
-- 
cgit v1.2.3


From 96fcef98d7bc0fd8940959077c009016aae56fd0 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sun, 18 Jul 2021 06:11:26 +0000
Subject: role config UI

---
 apioforum/db.py                           |  6 +++++
 apioforum/forum.py                        | 43 ++++++++++++++++++++++++++++---
 apioforum/roles.py                        | 41 +++++++++++++++++++++++++++++
 apioforum/templates/edit_permissions.html | 28 ++++++++++----------
 4 files changed, 102 insertions(+), 16 deletions(-)

diff --git a/apioforum/db.py b/apioforum/db.py
index 5ffd5d9..d94a707 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -135,6 +135,12 @@ CREATE TABLE role_config (
 
 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)
+);
 """
 ]
 
diff --git a/apioforum/forum.py b/apioforum/forum.py
index 4b7522c..ed7e2b7 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -8,7 +8,8 @@ from flask import (
 
 from .db import get_db
 from .mdrender import render
-from .roles import forum_perms, overridden_perms
+from .roles import get_forum_roles
+from .roles import permissions as role_permissions
 
 from sqlite3 import OperationalError
 import datetime
@@ -127,15 +128,51 @@ def edit_roles(forum_id):
         "SELECT * FROM role_config WHERE forum = ? ORDER BY ID ASC",
         (forum_id,)).fetchall()
 
+    if request.method == "POST":
+        for config in role_configs:
+            if '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=["the","test","placeholder"],
+            other_roles=other_roles
             )
 
 @bp.route("/<int:forum_id>/roles/new",methods=["POST"])
 def add_role(forum_id):
-    return "placeholder"
+    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))
 
 @bp.route("/search")
 def search():
diff --git a/apioforum/roles.py b/apioforum/roles.py
index 71efcbd..ae193a7 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -23,4 +23,45 @@ def get_role_config(forum_id, role):
             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, user):
+    db = get_db()
+    
+    fid = forum_id
+    the = None
+    while the == None and fid != None:
+        the = db.execute("""
+            SELECT * FROM role_assignments
+            WHERE forum = ? AND user = ?;
+            """, (fid,role)).fetchone()
+        fid = db.execute("""
+            SELECT * FROM forums WHERE id = ?
             """).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)
diff --git a/apioforum/templates/edit_permissions.html b/apioforum/templates/edit_permissions.html
index e79c0c7..1e4e848 100644
--- a/apioforum/templates/edit_permissions.html
+++ b/apioforum/templates/edit_permissions.html
@@ -9,11 +9,9 @@
 	everyone's role is "other" by default.
 </p>
 <p>
-	here a set of permissions may be associated with any role.
+	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.
-	if no permissions are set for the role in any ancestor forum,
-	the permissions for the role "other" are used.
 </p>
 <form method="post" id="role_config">
 
@@ -23,11 +21,11 @@
 		{% macro perm(p, description, tooltip) %}
 			<input 
 				type="checkbox" 
-				id="{{role_config.role}}_{{p}}" 
-				name="{{role_config.role}}_{{p}}" 
+				id="perm_{{role_config.role}}_{{p}}" 
+				name="perm_{{role_config.role}}_{{p}}" 
 				{% if role_config[p] %}checked{% endif %}
 			/>
-			<label for="{{role_config.role}}_{{p}}" title="{{tooltip}}">
+			<label for="perm_{{role_config.role}}_{{p}}" title="{{tooltip}}">
 				{{- description -}}
 			</label>
 			<br/>
@@ -47,15 +45,23 @@
 		{{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.")}}
+		<input type="hidden" name="roleconfig_{{role_config.role}}" value="present"/>
 		{% 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 %}
 	</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>
@@ -68,16 +74,12 @@
 		</li>
 		{% endfor %}
 		<li>
-			<form action="{{url_for('forum.add_role',forum_id=forum.id,role_name=role)}}" method="POST" style="display:inline">
-				<input type="text" name="role" class="role-input" placeholder="role name"/>
+			<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>
 
-<p>confirm changes?</p>
-<input type="submit" value="confirm" form="role_config">
-<a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
-
 {% endblock %}
-- 
cgit v1.2.3


From 3ea7198a166ac161df82a35c135a59bbd67ee645 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sat, 31 Jul 2021 10:16:24 +0000
Subject: "just commit" - ubq

---
 apioforum/__init__.py               |  2 ++
 apioforum/forum.py                  | 42 ++++++++++++++++++++++++++-----------
 apioforum/roles.py                  |  9 ++++++++
 apioforum/templates/view_forum.html | 10 +++++++--
 4 files changed, 49 insertions(+), 14 deletions(-)

diff --git a/apioforum/__init__.py b/apioforum/__init__.py
index 30dd813..7c99c0c 100644
--- a/apioforum/__init__.py
+++ b/apioforum/__init__.py
@@ -48,6 +48,8 @@ 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, 
+    app.jinja_env.globals.update(has_permission=has_permission,is_bureaucrat=is_bureaucrat)
 
     from .mdrender import render
     @app.template_filter('md')
diff --git a/apioforum/forum.py b/apioforum/forum.py
index ed7e2b7..5c6f5bf 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -8,8 +8,7 @@ from flask import (
 
 from .db import get_db
 from .mdrender import render
-from .roles import get_forum_roles
-from .roles import permissions as role_permissions
+from .roles import get_forum_roles,has_permission,is_bureaucrat
 
 from sqlite3 import OperationalError
 import datetime
@@ -32,10 +31,30 @@ def forum_path(forum_id):
     ancestors.reverse()
     return ancestors
 
-@bp.route("/<int:forum_id>")
-def view_forum(forum_id):
+def forum_route(relative_path, **kwargs):
+    def decorator(f):
+        path = "/<int:forum_id>"
+        if relative_path != "":
+            path += "/" + relative_path
+
+        @bp.route(path, **kwargs)
+        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)
+
+def requires_permission(permission):
+    def decorator(f):
+        def wrapper(forum, *args, **kwargs):
+            if not has_permission(forum['id'], g.user, permission):
+                abort(403)
+
+@forum_route("")
+def view_forum(forum):
     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,
@@ -49,7 +68,7 @@ def view_forum(forum_id):
         INNER JOIN number_of_posts ON number_of_posts.thread = threads.id
         WHERE threads.forum = ?
         ORDER BY threads.updated DESC;
-        """,(forum_id,)).fetchall()
+        """,(forum['id'],)).fetchall()
     thread_tags = {}
     #todo: somehow optimise this
     for thread in threads:
@@ -66,7 +85,7 @@ def view_forum(forum_id):
             WHERE parent = ?
             GROUP BY forums.id
             ORDER BY name ASC
-            """,(forum_id,)).fetchall()
+            """,(forum['id'],)).fetchall()
     subforums = []
     for s in subforums_rows:
         a={}
@@ -75,7 +94,6 @@ def view_forum(forum_id):
             a['updated'] = datetime.datetime.fromisoformat(a['updated'])
         subforums.append(a)
         
-
     return render_template("view_forum.html",
             forum=forum,
             subforums=subforums,
@@ -83,10 +101,10 @@ def view_forum(forum_id):
             thread_tags=thread_tags,
             )
 
-@bp.route("/<int:forum_id>/create_thread",methods=("GET","POST"))
-def create_thread(forum_id):
+@forum_route("create_thread",methods=("GET","POST"))
+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'))
@@ -106,7 +124,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(
diff --git a/apioforum/roles.py b/apioforum/roles.py
index ae193a7..ab273c8 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -65,3 +65,12 @@ def get_forum_roles(forum_id):
             SELECT * FROM role_config WHERE forum = ?
             """,(a['id'],)).fetchall()
     return set(r['role'] for r in configs)
+
+def has_permission(forum_id, user, permission):
+    role = get_user_role(forum_id, user) if user != None else "other"
+    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/templates/view_forum.html b/apioforum/templates/view_forum.html
index d3d09e1..98d2110 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -8,10 +8,11 @@
 {%endblock%}
 
 {%block content%}
-{% if forum.description %}
 {{forum.description|md|safe}}
-<hr/>
+{% if is_bureaucrat(forum.id, g.user) %}
+	<p><a class="actionbutton" href="{{url_for('forum.edit_roles')}}">role/permission settings</a></p>
 {% endif %}
+<hr/>
 
 {% if subforums %}
 <h2>subforæ</h2>
@@ -43,6 +44,8 @@
 {% else %}
 <p>please log in to create a new thread</p>
 {% endif %}
+
+{% if has_permission(forum.id, g.user, "p_view_threads") %}
 <div class="thread-list">
 	{%for thread in threads%}
 		<div class="listing">
@@ -80,5 +83,8 @@
 		</div>
 	{%endfor%}
 </div>
+{% else %}
+<p>you do not have permission to view threads in this forum</p>
+{% endif %}
 
 {%endblock%}
-- 
cgit v1.2.3


From fd04b1ea444b2b77cb56ed7a67b8ac2225cfa6bd Mon Sep 17 00:00:00 2001
From: ubq323 <ubq323>
Date: Sat, 31 Jul 2021 19:52:57 +0000
Subject: fix typos and syntax errors mainly, i think

---
 apioforum/__init__.py               | 2 +-
 apioforum/db.py                     | 3 ++-
 apioforum/forum.py                  | 8 ++++++--
 apioforum/roles.py                  | 6 +++---
 apioforum/templates/view_forum.html | 2 +-
 5 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/apioforum/__init__.py b/apioforum/__init__.py
index 7c99c0c..f28471f 100644
--- a/apioforum/__init__.py
+++ b/apioforum/__init__.py
@@ -48,7 +48,7 @@ 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, 
+    from .roles import has_permission, is_bureaucrat
     app.jinja_env.globals.update(has_permission=has_permission,is_bureaucrat=is_bureaucrat)
 
     from .mdrender import render
diff --git a/apioforum/db.py b/apioforum/db.py
index d94a707..cfb5646 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -139,7 +139,8 @@ 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)
+    forum NOT NULL REFERENCES forums(id),
+    role TEXT NOT NULL
 );
 """
 ]
diff --git a/apioforum/forum.py b/apioforum/forum.py
index 5c6f5bf..1c9b4ed 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -8,10 +8,10 @@ from flask import (
 
 from .db import get_db
 from .mdrender import render
-from .roles import get_forum_roles,has_permission,is_bureaucrat
-
+from .roles import get_forum_roles,has_permission,is_bureaucrat, permissions as role_permissions
 from sqlite3 import OperationalError
 import datetime
+import functools
 
 bp = Blueprint("forum", __name__, url_prefix="/")
 
@@ -38,6 +38,7 @@ def forum_route(relative_path, **kwargs):
             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 = ?",
@@ -46,8 +47,11 @@ def forum_route(relative_path, **kwargs):
                 abort(404)
             return f(forum, *args, **kwargs)
 
+    return decorator
+
 def requires_permission(permission):
     def decorator(f):
+        @functools.wraps(f)
         def wrapper(forum, *args, **kwargs):
             if not has_permission(forum['id'], g.user, permission):
                 abort(403)
diff --git a/apioforum/roles.py b/apioforum/roles.py
index ab273c8..6d20316 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -24,7 +24,7 @@ def get_role_config(forum_id, role):
             """, (fid,role)).fetchone()
         fid = db.execute("""
             SELECT * FROM forums WHERE id = ?
-            """(fid,)).fetchone()['parent']
+            """,(fid,)).fetchone()['parent']
     if the == None:
         if role == "other":
             raise(RuntimeError(
@@ -43,10 +43,10 @@ def get_user_role(forum_id, user):
         the = db.execute("""
             SELECT * FROM role_assignments
             WHERE forum = ? AND user = ?;
-            """, (fid,role)).fetchone()
+            """,(fid,user)).fetchone()
         fid = db.execute("""
             SELECT * FROM forums WHERE id = ?
-            """).fetchone()['parent']
+            """,(fid,)).fetchone()['parent']
     return the['role'] if the != None else 'other'
 
 def get_forum_roles(forum_id):
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index 98d2110..c5666c8 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -10,7 +10,7 @@
 {%block content%}
 {{forum.description|md|safe}}
 {% if is_bureaucrat(forum.id, g.user) %}
-	<p><a class="actionbutton" href="{{url_for('forum.edit_roles')}}">role/permission settings</a></p>
+	<p><a class="actionbutton" href="{{url_for('forum.edit_roles',forum_id=forum.id)}}">role/permission settings</a></p>
 {% endif %}
 <hr/>
 
-- 
cgit v1.2.3


From 76cb3a6be912e55ffe7f6e7c221000f57cff6d4a Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Thu, 5 Aug 2021 11:28:53 +0000
Subject: make many of the permissions do things. somewhat functional menus for
 role configuration and assignment. big brother thoughtcrime message deletion.
 mark roles next to usernames on posts. other things I may have forgot

---
 apioforum/__init__.py                     |   7 +-
 apioforum/db.py                           |   4 ++
 apioforum/forum.py                        |  98 ++++++++++++++++++++++-----
 apioforum/roles.py                        |   2 +
 apioforum/static/style.css                |  38 +++++++++--
 apioforum/templates/common.html           |  45 ++++++++++---
 apioforum/templates/delete_thread.html    |  18 +++++
 apioforum/templates/edit_permissions.html |   9 +--
 apioforum/templates/role_assignment.html  |  53 +++++++++++++++
 apioforum/templates/view_forum.html       |  27 +++++---
 apioforum/templates/view_thread.html      |   9 ++-
 apioforum/thread.py                       | 107 +++++++++++++++++++-----------
 12 files changed, 331 insertions(+), 86 deletions(-)
 create mode 100644 apioforum/templates/delete_thread.html
 create mode 100644 apioforum/templates/role_assignment.html

diff --git a/apioforum/__init__.py b/apioforum/__init__.py
index f28471f..1d96d8c 100644
--- a/apioforum/__init__.py
+++ b/apioforum/__init__.py
@@ -48,8 +48,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
-    app.jinja_env.globals.update(has_permission=has_permission,is_bureaucrat=is_bureaucrat)
+    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 cfb5646..d501159 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -127,6 +127,7 @@ CREATE TABLE role_config (
     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,
@@ -142,6 +143,9 @@ CREATE TABLE role_assignments (
     forum NOT NULL REFERENCES forums(id),
     role TEXT NOT NULL
 );
+""",
+"""
+ALTER TABLE posts ADD COLUMN deleted NOT NULL DEFAULT 0;
 """
 ]
 
diff --git a/apioforum/forum.py b/apioforum/forum.py
index 1c9b4ed..9d84a69 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -3,12 +3,12 @@
 
 from flask import (
     Blueprint, render_template, request,
-    g, redirect, url_for, flash
+    g, redirect, url_for, flash, abort
 )
 
 from .db import get_db
 from .mdrender import render
-from .roles import get_forum_roles,has_permission,is_bureaucrat, permissions as role_permissions
+from .roles import get_forum_roles,has_permission,is_bureaucrat,get_user_role, permissions as role_permissions
 from sqlite3 import OperationalError
 import datetime
 import functools
@@ -55,6 +55,17 @@ def requires_permission(permission):
         def wrapper(forum, *args, **kwargs):
             if not has_permission(forum['id'], g.user, permission):
                 abort(403)
+            return f(forum, *args, **kwargs)
+        return wrapper
+    return decorator
+
+def requires_bureaucrat(f):
+    @functools.wraps(f)
+    def wrapper(forum, *args, **kwargs):
+        if not is_bureaucrat(forum['id'], g.user):
+            abort(403)
+        return f(forum, *args, **kwargs)
+    return wrapper
 
 @forum_route("")
 def view_forum(forum):
@@ -66,7 +77,8 @@ def view_forum(forum):
             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
@@ -142,13 +154,13 @@ def create_thread(forum):
         
     return render_template("create_thread.html")
 
-@bp.route("/<int:forum_id>/roles",methods=("GET","POST"))
-def edit_roles(forum_id):
+@forum_route("roles",methods=("GET","POST"))
+@requires_bureaucrat
+def edit_roles(forum):
     db = get_db()
-    forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum_id,)).fetchone()
     role_configs = db.execute(
         "SELECT * FROM role_config WHERE forum = ? ORDER BY ID ASC",
-        (forum_id,)).fetchall()
+        (forum['id'],)).fetchall()
 
     if request.method == "POST":
         for config in role_configs:
@@ -160,13 +172,13 @@ def edit_roles(forum_id):
                         UPDATE role_config SET {p} = ?
                             WHERE forum = ? AND role = ?;
                         """, 
-                        (permission_setting,forum_id, config['role']))
+                        (permission_setting,forum['id'], config['role']))
         db.commit()
         flash('roles sucessfully enroled')
-        return redirect(url_for('forum.view_forum',forum_id=forum_id))
+        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]
+    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,
@@ -174,27 +186,79 @@ def edit_roles(forum_id):
             other_roles=other_roles
             )
 
-@bp.route("/<int:forum_id>/roles/new",methods=["POST"])
-def add_role(forum_id):
+@forum_route("roles/new",methods=["POST"])
+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))
+        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))
+        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()
+        """,(forum['id'],name)).fetchone()
     if not existing_config:
         db.execute("INSERT INTO role_config (forum,role) VALUES (?,?)",
-                (forum_id,name))
+                (forum['id'],name))
         db.commit()
-    return redirect(url_for('forum.edit_roles',forum_id=forum_id))
+    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 != "":
+            abort(403)
+        existing = db.execute("SELECT * FROM role_assignments WHERE user = ?;",(username,)).fetchone()
+        if existing:
+            if role == "":
+                db.execute("DELETE FROM role_assignments WHERE user = ?;",(username,))
+            else:
+                db.execute("UPDATE role_assignments SET role = ? WHERE user = ?;",(role,username))
+            db.commit()
+        elif role != "":
+            db.execute("INSERT INTO role_assignments (user,role) VALUES (?,?);",(username,role))
+            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)
+        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,forum_roles=roles)
 
 @bp.route("/search")
 def search():
diff --git a/apioforum/roles.py b/apioforum/roles.py
index 6d20316..bda6704 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -5,6 +5,7 @@ permissions = [
     "p_create_threads",
     "p_reply_threads",
     "p_manage_threads",
+    "p_delete_posts",
     "p_view_threads",
     "p_vote",
     "p_create_polls",
@@ -68,6 +69,7 @@ def get_forum_roles(forum_id):
 
 def has_permission(forum_id, user, permission):
     role = get_user_role(forum_id, user) if user != None else "other"
+    if role == "bureaucrat": return True
     config = get_role_config(forum_id, role)
     return config[permission]
 
diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index 3813d63..931ac9a 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 { font-size: smaller; }
-.post-heading,a.username {
+.post-heading,.username {
 	color: hsl(0,0%,25%); 
 }
-a.username {
+.username {
     font-weight: bold;
     text-decoration: underline;
 }
@@ -34,6 +34,23 @@ 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%); 
+}
+
 .thread-top-bar, .user-top-bar {
     margin-bottom: 4px;
 }
@@ -79,7 +96,16 @@ dt { font-weight: bold }
 img { max-width: 100% }
 
 
-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 }
@@ -185,6 +211,8 @@ label { user-select: none; }
 
 fieldset { margin-bottom: 15px; }
 
+.warning { color: red; font-weight: bold }
+
 .search-form {
     display: inline-block;
 }
@@ -195,7 +223,9 @@ fieldset { margin-bottom: 15px; }
     border: 1px solid black;
 }
 
-.role-input { width: 12ch; }
+.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 b0bf713..9e60e81 100644
--- a/apioforum/templates/common.html
+++ b/apioforum/templates/common.html
@@ -6,28 +6,55 @@
 	{{url_for('thread.view_thread', thread_id=post.thread)}}#post_{{post.id}}
 {%- endmacro %}
 
-{% macro disp_post(post, buttons=False) %}
-<div class="post" id="post_{{post.id}}">
+{% macro disp_post(post, buttons=False, forum=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 and 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>
+        {% 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 %}
         {% endif %}
 		<a class="post-anchor-link" href="{{post_url(post)}}">#{{post.id}}</a>
         </span>
     </div>
     <div class="post-content">
-        {{ post.content|md|safe }}
+		{% if not post.deleted %}
+			{{ post.content|md|safe }}
+		{% else %}
+			this post never existed.
+		{% endif %}
     </div>
 </div>
 {% endmacro %}
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_permissions.html b/apioforum/templates/edit_permissions.html
index 1e4e848..f91c710 100644
--- a/apioforum/templates/edit_permissions.html
+++ b/apioforum/templates/edit_permissions.html
@@ -1,5 +1,4 @@
 {% extends 'base.html' %}
-{% from 'common.html' import tag %}
 {% block header %}<h1>{% block title %}role permissions for '{{forum.name}}'{% endblock %}</h1>{% endblock %}
 {% block content %}
 <p>
@@ -37,11 +36,13 @@
 		{{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 delete, lock, or modify the title/tags for 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 create poll threads")}}
+				"allow users with the role to add a poll to a thread")}}
 		{{perm("p_vote","vote",
-				"allow users with the role to vote on poll threads")}}
+				"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.")}}
diff --git a/apioforum/templates/role_assignment.html b/apioforum/templates/role_assignment.html
new file mode 100644
index 0000000..d56c060
--- /dev/null
+++ b/apioforum/templates/role_assignment.html
@@ -0,0 +1,53 @@
+{% extends 'base.html' %}
+{% 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 %}
+<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>
+
+{% if invalid_user %}
+	<p>requested user does not exist.</p>
+	<p>
+		<a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
+	</p>
+{% elif user %}
+<hr/>
+<form method="post">
+	<p>{{user}}'s role in this forum is "{{role}}"</p>
+	{% if role == "other" or is_bureaucrat(forum.id, g.user) %}
+		<label for="role">assign role: </label>
+		<select name="role" id="role" value="">
+			<option value="">(no assigned role)</option>
+			{% for role in forum_roles %}
+				<option value="{{role}}">{{role}}</option>
+			{% endfor %}
+		</select>
+	{% else %}
+		<p>you do not have permission to change the role of this user</p>
+	{% endif %}
+	<p>confirm changes?</p>
+	<p>
+		<input type="submit" value="confirm">
+		<a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
+	</p>
+</form>
+{% else %}
+<p>
+	<a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
+</p>
+{% endif %}
+
+{% endblock %}
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index c5666c8..a3563be 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -1,7 +1,7 @@
 {% extends 'base.html' %}
 {% from 'common.html' import ts, tag, disp_user, post_url, forum_breadcrumb %}
 {% 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 %}
@@ -9,11 +9,15 @@
 
 {%block content%}
 {{forum.description|md|safe}}
-{% if is_bureaucrat(forum.id, g.user) %}
-	<p><a class="actionbutton" href="{{url_for('forum.edit_roles',forum_id=forum.id)}}">role/permission settings</a></p>
-{% endif %}
-<hr/>
-
+<p>
+	{% if is_bureaucrat(forum.id, g.user) %}
+		<a class="actionbutton" href="{{url_for('forum.edit_roles',forum_id=forum.id)}}">role/permission settings</a>
+		<a class="actionbutton" href="{{url_for('forum.view_user_role',forum_id=forum.id)}}">assign roles</a>
+	{% endif %}
+	{% if not is_bureaucrat(forum.id, g.user) and has_permission(forum.id, g.user, "p_approve") %}
+		<a class="actionbutton" href="{{url_for('forum.view_user_role',forum_id=forum.id)}}">approve users</a>
+	{% endif %}
+</p>
 {% if subforums %}
 <h2>subforæ</h2>
 <div class="forum-list">
@@ -67,7 +71,7 @@
 					{{ 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">
@@ -79,7 +83,14 @@
 						</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 %}
 		</div>
 	{%endfor%}
 </div>
diff --git a/apioforum/templates/view_thread.html b/apioforum/templates/view_thread.html
index dd41d87..da8df74 100644
--- a/apioforum/templates/view_thread.html
+++ b/apioforum/templates/view_thread.html
@@ -1,16 +1,19 @@
 {% from 'common.html' import disp_post,tag,thread_breadcrumb %}
 {% 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 %}
 
 {%block content%}
 <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">
@@ -22,7 +25,7 @@
 
 <div class="posts">
     {% for post in posts %}
-	{{ disp_post(post, True) }}
+	{{ disp_post(post, buttons=True, forum=thread.forum) }}
     {% endfor %}
 </div>
 {% if g.user %}
diff --git a/apioforum/thread.py b/apioforum/thread.py
index 4bb3c86..1291adf 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -5,6 +5,7 @@ from flask import (
     url_for, flash
 )
 from .db import get_db
+from .roles import has_permission
 
 bp = Blueprint("thread", __name__, url_prefix="/thread")
 
@@ -17,66 +18,94 @@ def view_thread(thread_id):
     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 thread = ? ORDER BY created ASC;",
-            (thread_id,)
-        ).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()
-        return render_template("view_thread.html",posts=posts,thread=thread,tags=tags)
+    if not has_permission(thread['forum'], g.user, "p_view_threads"):
+        abort(403)
+    posts = db.execute(
+        "SELECT * FROM posts WHERE thread = ? ORDER BY created ASC;",
+        (thread_id,)
+    ).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()
+    return render_template("view_thread.html",posts=posts,thread=thread,tags=tags)
 
 @bp.route("/<int:thread_id>/create_post", methods=("POST",))
 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")
     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:
-            cur = db.cursor()
-            cur.execute(
-                "INSERT INTO posts (thread,author,content,created) VALUES (?,?,?,current_timestamp);",
-                (thread_id,g.user,content)
-            )
-            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) VALUES (?,?,?,current_timestamp);",
+            (thread_id,g.user,content)
+        )
+        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=post["thread"]))
+    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):
@@ -117,7 +146,7 @@ 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 g.user != thread['creator'] and not has_permission(thread['forum'], g.user, "g_manage_threads"):
         err = "you can only configure threads that you own"
 
     if err is not None:
-- 
cgit v1.2.3


From 6d7246a9496015a00538c00689d43fad241fbcca Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Thu, 5 Aug 2021 21:47:30 +0000
Subject: fix role assignment UI

---
 apioforum/forum.py                       | 21 +++++++++++++--------
 apioforum/templates/role_assignment.html | 29 ++++++++++++++---------------
 2 files changed, 27 insertions(+), 23 deletions(-)

diff --git a/apioforum/forum.py b/apioforum/forum.py
index 9d84a69..f86629d 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -235,13 +235,11 @@ def edit_user_role(forum, username):
             abort(403)
         existing = db.execute("SELECT * FROM role_assignments WHERE user = ?;",(username,)).fetchone()
         if existing:
-            if role == "":
-                db.execute("DELETE FROM role_assignments WHERE user = ?;",(username,))
-            else:
-                db.execute("UPDATE role_assignments SET role = ? WHERE user = ?;",(role,username))
-            db.commit()
-        elif role != "":
-            db.execute("INSERT INTO role_assignments (user,role) VALUES (?,?);",(username,role))
+            db.execute("DELETE FROM role_assignments WHERE user = ?;",(username,))
+        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']))
@@ -250,6 +248,12 @@ def edit_user_role(forum, username):
         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 = ?;",(username,)).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'])
@@ -258,7 +262,8 @@ def edit_user_role(forum, username):
         else:
             roles = ["approved"]
         return render_template("role_assignment.html",
-                forum=forum,user=username,role=role,forum_roles=roles)
+                forum=forum,user=username,role=role,
+                assigned_role=assigned_role,forum_roles=roles)
 
 @bp.route("/search")
 def search():
diff --git a/apioforum/templates/role_assignment.html b/apioforum/templates/role_assignment.html
index d56c060..b212606 100644
--- a/apioforum/templates/role_assignment.html
+++ b/apioforum/templates/role_assignment.html
@@ -18,36 +18,35 @@
 	<input type="submit" value="view"/>
 </form>
 
+{% set can_change = not invalid_user and user %}
 {% if invalid_user %}
 	<p>requested user does not exist.</p>
-	<p>
-		<a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
-	</p>
 {% elif user %}
 <hr/>
-<form method="post">
+<form method="post" id="role-form">
 	<p>{{user}}'s role in this forum is "{{role}}"</p>
-	{% if role == "other" or is_bureaucrat(forum.id, g.user) %}
-		<label for="role">assign role: </label>
-		<select name="role" id="role" value="">
+	{% 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="">(no assigned role)</option>
 			{% for role in forum_roles %}
-				<option value="{{role}}">{{role}}</option>
+				<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 %}
-	<p>confirm changes?</p>
-	<p>
-		<input type="submit" value="confirm">
-		<a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
-	</p>
 </form>
-{% else %}
+{% 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>
-{% endif %}
 
 {% endblock %}
-- 
cgit v1.2.3


From 52c63cddb3f7860862af6a2185a728baf7593cc7 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Fri, 6 Aug 2021 01:35:37 +0000
Subject: fix roles even more; forum creation and configuration

---
 apioforum/db.py                          |  3 ++
 apioforum/forum.py                       | 78 ++++++++++++++++++++++++++++++--
 apioforum/roles.py                       |  9 +++-
 apioforum/static/style.css               |  2 +-
 apioforum/templates/common.html          |  4 ++
 apioforum/templates/edit_forum.html      | 27 +++++++++++
 apioforum/templates/role_assignment.html |  2 +-
 apioforum/templates/view_forum.html      | 20 ++++++--
 apioforum/templates/view_unlisted.html   | 24 ++++++++++
 apioforum/user.py                        |  7 ++-
 10 files changed, 161 insertions(+), 15 deletions(-)
 create mode 100644 apioforum/templates/edit_forum.html
 create mode 100644 apioforum/templates/view_unlisted.html

diff --git a/apioforum/db.py b/apioforum/db.py
index d501159..899c6b4 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -146,6 +146,9 @@ CREATE TABLE role_assignments (
 """,
 """
 ALTER TABLE posts ADD COLUMN deleted NOT NULL DEFAULT 0;
+""",
+"""
+ALTER TABLE forums ADD COLUMN unlisted NOT NULL DEFAULT 0;
 """
 ]
 
diff --git a/apioforum/forum.py b/apioforum/forum.py
index f86629d..410bee5 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -98,7 +98,7 @@ def view_forum(forum):
     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()
@@ -109,12 +109,19 @@ def view_forum(forum):
         if a['updated'] is not None:
             a['updated'] = datetime.datetime.fromisoformat(a['updated'])
         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
             )
 
 @forum_route("create_thread",methods=("GET","POST"))
@@ -232,15 +239,18 @@ def edit_user_role(forum, username):
             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 = ?;",(username,)).fetchone()
+        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 = ?;",(username,))
+            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()
+        db.commit()
         flash("role assigned assignedly")
         return redirect(url_for('forum.view_forum',forum_id=forum['id']))
     else:
@@ -249,7 +259,8 @@ def edit_user_role(forum, username):
             return render_template("role_assignment.html",
                     forum=forum,user=username,invalid_user=True)
         r = db.execute(
-                "SELECT role FROM role_assignments WHERE user = ?;",(username,)).fetchone()
+                "SELECT role FROM role_assignments WHERE user = ? AND forum = ?;",
+                    (username,forum['id'])).fetchone()
         if not r:
             assigned_role = ""
         else:
@@ -265,6 +276,63 @@ def edit_user_role(forum, username):
                 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"]
+        unlisted = "unlisted" in request.form
+        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 = ?, unlisted = ? WHERE id = ?",
+                    (name,desc,forum['id']))
+            fid = forum['id']
+        else:
+            cur = db.cursor()
+            cur.execute(
+                "INSERT INTO forums (name,description,parent,unlisted) VALUES (?,?,?,?)",
+                    (name,desc,forum['id'],unlisted))
+            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")
+@requires_bureaucrat
+def view_unlisted(forum):
+    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
index bda6704..ae47e31 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -40,11 +40,16 @@ def get_user_role(forum_id, user):
     
     fid = forum_id
     the = None
-    while the == None and fid != None:
-        the = db.execute("""
+    while fid != None:
+        r = db.execute("""
             SELECT * FROM role_assignments
             WHERE forum = ? AND user = ?;
             """,(fid,user)).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']
diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index 931ac9a..2ed2e7a 100644
--- a/apioforum/static/style.css
+++ b/apioforum/static/style.css
@@ -188,7 +188,7 @@ nav#navbar .links { display: flex; }
 }
 .actionbutton { color:blue }
 
-.new-post-box {
+.new-post-box, .forum-desc-box {
     height:20em;
     resize:vertical;
     width:100%;
diff --git a/apioforum/templates/common.html b/apioforum/templates/common.html
index 9e60e81..7144667 100644
--- a/apioforum/templates/common.html
+++ b/apioforum/templates/common.html
@@ -67,6 +67,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/edit_forum.html b/apioforum/templates/edit_forum.html
new file mode 100644
index 0000000..c8027e7
--- /dev/null
+++ b/apioforum/templates/edit_forum.html
@@ -0,0 +1,27 @@
+{% 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>
+	<input type="checkbox" id="unlisted" name="unlisted"/>
+	<label for="unlisted">unlisted?</label>
+	<p>
+		<input type="submit" value="confirm">
+		<a href="{{cancel_link}}">cancel</a>
+	</p>
+</form>
+{% endblock %}
diff --git a/apioforum/templates/role_assignment.html b/apioforum/templates/role_assignment.html
index b212606..74dc3cd 100644
--- a/apioforum/templates/role_assignment.html
+++ b/apioforum/templates/role_assignment.html
@@ -29,7 +29,7 @@
 	{% if can_change %}
 		<label for="role">assigned role: </label>
 		<select name="role" id="role" autocomplete="off">
-			<option value="">(no assigned role)</option>
+			<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 %}>
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index a3563be..863f91c 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -1,5 +1,5 @@
 {% extends 'base.html' %}
-{% from 'common.html' import ts, tag, disp_user, post_url, forum_breadcrumb %}
+{% from 'common.html' import ts, tag, disp_user, post_url, forum_breadcrumb, ab %}
 {% block header %}
 <h1>{% block title %}{{forum.name}}{% endblock %} <span class="thing-id">#{{forum.id}}</span></h1>
 {% if forum.id != 1 %}
@@ -9,13 +9,25 @@
 
 {%block content%}
 {{forum.description|md|safe}}
+{% if bureaucrats|length > 0 %}
+	<p>
+		bureaucrats in this forum:
+		{% for b in bureaucrats %}
+			{{disp_user(b)}}
+		{% endfor %}
+	</p>
+{% endif %}
 <p>
 	{% if is_bureaucrat(forum.id, g.user) %}
-		<a class="actionbutton" href="{{url_for('forum.edit_roles',forum_id=forum.id)}}">role/permission settings</a>
-		<a class="actionbutton" href="{{url_for('forum.view_user_role',forum_id=forum.id)}}">assign roles</a>
+		{{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") %}
-		<a class="actionbutton" href="{{url_for('forum.view_user_role',forum_id=forum.id)}}">approve users</a>
+		{{ab("approve users",url_for('forum.view_user_role',forum_id=forum.id))}}
 	{% endif %}
 </p>
 {% if subforums %}
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/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"])
-- 
cgit v1.2.3


From 6d2a72726f95ba762ede5c25aff8b73573fb77c0 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Fri, 6 Aug 2021 04:25:55 +0000
Subject: fix forum edit page

---
 apioforum/forum.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apioforum/forum.py b/apioforum/forum.py
index cae4eab..d177b16 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -332,7 +332,7 @@ def forum_config_page(forum, create=False):
             return redirect(url_for('forum.edit_forum',forum_id=forum['id']))
         if not create:
             db.execute("UPDATE forums SET name = ?, description = ?, unlisted = ? WHERE id = ?",
-                    (name,desc,forum['id']))
+                    (name,desc,unlisted,forum['id']))
             fid = forum['id']
         else:
             cur = db.cursor()
-- 
cgit v1.2.3


From bd7a53ba3daf8853707d6df511cc1e31d2a850a3 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Fri, 6 Aug 2021 04:54:23 +0000
Subject: fix thread edit page

---
 apioforum/templates/config_thread.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apioforum/templates/config_thread.html b/apioforum/templates/config_thread.html
index 7403614..383cc9c 100644
--- a/apioforum/templates/config_thread.html
+++ b/apioforum/templates/config_thread.html
@@ -29,7 +29,7 @@
 <a href="{{url_for('thread.view_thread',thread_id=thread.id)}}">cancel</a>
 </form>
 
-{% if thread.poll is none and has_permission(thread.forum, g.user, "p_create_polls" %}
+{% if thread.poll is none and has_permission(thread.forum, g.user, "p_create_polls") %}
 <h2>create poll</h2>
 <form method="post" action="{{url_for('thread.create_poll',thread_id=thread.id)}}">
     <fieldset>
-- 
cgit v1.2.3


From a4d99164de42603b83b2a7ac6e594e5925108a32 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Fri, 6 Aug 2021 22:54:59 +0000
Subject: logged out users are considered to have no permissions unless in a
 specific instance login is not required in which case they are treated as
 having the role "other".

---
 apioforum/roles.py                  | 5 +++--
 apioforum/templates/view_forum.html | 2 +-
 apioforum/thread.py                 | 2 +-
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/apioforum/roles.py b/apioforum/roles.py
index ae47e31..1e9b206 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -72,8 +72,9 @@ def get_forum_roles(forum_id):
             """,(a['id'],)).fetchall()
     return set(r['role'] for r in configs)
 
-def has_permission(forum_id, user, permission):
-    role = get_user_role(forum_id, user) if user != None else "other"
+def has_permission(forum_id, user, permission, login_required=True):
+    if user == None and login_required: return False
+    role = get_user_role(forum_id, user) if user else "other"
     if role == "bureaucrat": return True
     config = get_role_config(forum_id, role)
     return config[permission]
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index ff1af9b..a4ffac6 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -76,7 +76,7 @@
 please log in to create a new thread
 {% endif %}
 
-{% if has_permission(forum.id, g.user, "p_view_threads") %}
+{% if has_permission(forum.id, g.user, "p_view_threads", login_required=False) %}
 <div class="thread-list">
 	{%for thread in threads%}
 		<div class="listing">
diff --git a/apioforum/thread.py b/apioforum/thread.py
index 0b0804e..a3a122a 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -21,7 +21,7 @@ def view_thread(thread_id):
     thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone()
     if thread is None:
         abort(404)
-    if not has_permission(thread['forum'], g.user, "p_view_threads"):
+    if not has_permission(thread['forum'], g.user, "p_view_threads", False):
         abort(403)
     posts = db.execute("""
         SELECT * FROM posts
-- 
cgit v1.2.3


From 7e5abdf79fb8358ed28d69241a929e3af4841d5b Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sat, 7 Aug 2021 00:24:53 +0000
Subject: random CSS thing that is peeving me

---
 apioforum/static/style.css          | 6 +++++-
 apioforum/templates/edit_forum.html | 2 +-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index 62d643c..280749b 100644
--- a/apioforum/static/style.css
+++ b/apioforum/static/style.css
@@ -196,7 +196,10 @@ nav#navbar .links { display: flex; }
     content: "]";
     color: grey;
 }
-.actionbutton { color:blue }
+.actionbutton { 
+	color:blue;
+	white-space: nowrap;
+}
 
 .new-post-box, .forum-desc-box {
     height:20em;
@@ -241,6 +244,7 @@ fieldset { margin-bottom: 15px; }
     font-size: .75rem;
     padding: 1px 3px;
     border: 1px solid black;
+	white-space: nowrap;
 }
 
 .md table {
diff --git a/apioforum/templates/edit_forum.html b/apioforum/templates/edit_forum.html
index c8027e7..32bfaf1 100644
--- a/apioforum/templates/edit_forum.html
+++ b/apioforum/templates/edit_forum.html
@@ -17,7 +17,7 @@
 		maxlength="6000"
 		required
 	>{{description}}</textarea>
-	<input type="checkbox" id="unlisted" name="unlisted"/>
+	<input type="checkbox" id="unlisted" name="unlisted" {% if unlisted %}checked{% endif %}/>
 	<label for="unlisted">unlisted?</label>
 	<p>
 		<input type="submit" value="confirm">
-- 
cgit v1.2.3


From 2cbb238d80533cf06b1b6a7e1353843a0f583dea Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sat, 7 Aug 2021 01:16:20 +0000
Subject: admin users are automatically bureaucrats

---
 apioforum/forum.py | 1 +
 apioforum/roles.py | 4 ++++
 2 files changed, 5 insertions(+)

diff --git a/apioforum/forum.py b/apioforum/forum.py
index d177b16..bbc43fe 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,
diff --git a/apioforum/roles.py b/apioforum/roles.py
index 1e9b206..bd913a0 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -37,6 +37,10 @@ def get_role_config(forum_id, role):
 
 def get_user_role(forum_id, user):
     db = get_db()
+    user = db.execute('SELECT * FROM users WHERE username = ?',
+            (user,)).fetchone()
+    if user == None: return "other"
+    if user['admin']: return "bureaucrat"
     
     fid = forum_id
     the = None
-- 
cgit v1.2.3


From 09f38fd67bae05c7998ab9573d5842d02df247d8 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sat, 7 Aug 2021 23:22:51 +0000
Subject: one is now able to remove a role configuration

---
 apioforum/forum.py                        |  6 +++++-
 apioforum/templates/edit_permissions.html | 10 ++++++++--
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/apioforum/forum.py b/apioforum/forum.py
index bbc43fe..270b328 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -214,7 +214,11 @@ def edit_roles(forum):
 
     if request.method == "POST":
         for config in role_configs:
-            if 'roleconfig_' + config['role'] in request.form:
+            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 
diff --git a/apioforum/templates/edit_permissions.html b/apioforum/templates/edit_permissions.html
index f91c710..c92c9a9 100644
--- a/apioforum/templates/edit_permissions.html
+++ b/apioforum/templates/edit_permissions.html
@@ -16,7 +16,7 @@
 
 {% for role_config in role_configs %}
 	<fieldset>
-	<legend id="config_{{role_config.role}}">{{role_config.role}}</legend>
+		<legend id="config_{{role_config.role}}">{{role_config.role}}</legend>
 		{% macro perm(p, description, tooltip) %}
 			<input 
 				type="checkbox" 
@@ -46,11 +46,17 @@
 		{{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.")}}
-		<input type="hidden" name="roleconfig_{{role_config.role}}" value="present"/>
 		{% 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 %}
-- 
cgit v1.2.3


From 5e46fa10619a8b4d0e7c635658aa36ca47c8a624 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sat, 7 Aug 2021 23:43:47 +0000
Subject: fix the issue in which everything is broken

---
 apioforum/roles.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/apioforum/roles.py b/apioforum/roles.py
index bd913a0..d8e59ba 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -35,10 +35,10 @@ def get_role_config(forum_id, role):
             return get_role_config(forum_id, "other")
     return the
 
-def get_user_role(forum_id, user):
+def get_user_role(forum_id, username):
     db = get_db()
     user = db.execute('SELECT * FROM users WHERE username = ?',
-            (user,)).fetchone()
+            (username,)).fetchone()
     if user == None: return "other"
     if user['admin']: return "bureaucrat"
     
@@ -48,7 +48,7 @@ def get_user_role(forum_id, user):
         r = db.execute("""
             SELECT * FROM role_assignments
             WHERE forum = ? AND user = ?;
-            """,(fid,user)).fetchone()
+            """,(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"
-- 
cgit v1.2.3


From 242f8cd10f51caa271bb4a5bdbab24f06edaf157 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sun, 8 Aug 2021 00:02:25 +0000
Subject: p_create_threads actually does the

---
 apioforum/forum.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/apioforum/forum.py b/apioforum/forum.py
index 270b328..ce0215c 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -168,6 +168,7 @@ def view_forum(forum):
             )
 
 @forum_route("create_thread",methods=("GET","POST"))
+@requires_permission("p_create_threads")
 def create_thread(forum):
     db = get_db()
     forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum['id'],)).fetchone()
-- 
cgit v1.2.3


From 591ca58043f0c4cb9c148eb058d0c0663ff87c5a Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sun, 8 Aug 2021 00:05:33 +0000
Subject: UI reflects p_create_threads

---
 apioforum/templates/view_forum.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index a4ffac6..290bb23 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -70,7 +70,7 @@
 
 <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 %}
 please log in to create a new thread
-- 
cgit v1.2.3


From 761e87bd3f5942199dc24ea93f455284ccb608fb Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sun, 8 Aug 2021 00:07:46 +0000
Subject: UI does not say "login required" when logged in

---
 apioforum/templates/view_forum.html | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index 290bb23..0994752 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -72,8 +72,10 @@
 <p>
 {% 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 %}
 
 {% if has_permission(forum.id, g.user, "p_view_threads", login_required=False) %}
-- 
cgit v1.2.3


From 6ccb87c7c0e735664061154ff6ca6980fa332083 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sun, 8 Aug 2021 00:15:59 +0000
Subject: fix things

---
 apioforum/templates/config_thread.html | 5 +++--
 apioforum/templates/view_forum.html    | 2 +-
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/apioforum/templates/config_thread.html b/apioforum/templates/config_thread.html
index 383cc9c..0795ccc 100644
--- a/apioforum/templates/config_thread.html
+++ b/apioforum/templates/config_thread.html
@@ -29,7 +29,8 @@
 <a href="{{url_for('thread.view_thread',thread_id=thread.id)}}">cancel</a>
 </form>
 
-{% if thread.poll is none and has_permission(thread.forum, g.user, "p_create_polls") %}
+{% 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)}}">
     <fieldset>
@@ -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/view_forum.html b/apioforum/templates/view_forum.html
index 0994752..c42b7b8 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -72,7 +72,7 @@
 <p>
 {% 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>
-{% elif has_permission(forum.id, g.user "p_create_threads", login_required=False) %}
+{% 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
-- 
cgit v1.2.3


From 8ae879fdfe18997a82535ed64acc40fe281d759c Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sun, 8 Aug 2021 00:35:40 +0000
Subject: one's role in a forum is now viewable

---
 apioforum/templates/view_forum.html | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index c42b7b8..0eada1a 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -21,6 +21,11 @@
 		</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)}}
-- 
cgit v1.2.3


From 9a1373022ea968aa84324eb6db8e3f3a001297d1 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sun, 8 Aug 2021 00:47:28 +0000
Subject: fix typo

---
 apioforum/thread.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apioforum/thread.py b/apioforum/thread.py
index a3a122a..e9deea8 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -314,7 +314,7 @@ 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'] and not has_permission(thread['forum'], g.user, "g_manage_threads"):
+    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:
-- 
cgit v1.2.3


From 4588c1526d6cb73b85f10e2c177d2686ebc9e26c Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sun, 8 Aug 2021 01:07:13 +0000
Subject: thread configuration cannot occur if one is not able to view the
 thread. unlisted forums are completely invisible to those without view
 permissions

---
 apioforum/forum.py  | 5 +++++
 apioforum/thread.py | 2 ++
 2 files changed, 7 insertions(+)

diff --git a/apioforum/forum.py b/apioforum/forum.py
index ce0215c..108f0ba 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -83,6 +83,11 @@ def requires_bureaucrat(f):
 
 @forum_route("")
 def view_forum(forum):
+    # user should not be able to see anything about the forum if it is unlisted
+    # and the user does not have permission to see things
+    if forum['unlisted'] and not has_permission(forum['id'], g.user, "p_view_threads"):
+        abort(403)
+
     db = get_db()
     threads = db.execute(
         """SELECT
diff --git a/apioforum/thread.py b/apioforum/thread.py
index e9deea8..3c054d7 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -314,6 +314,8 @@ def config_thread(thread_id):
     err = None
     if g.user is None:
         err = "you need to be logged in to do that"
+    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"
 
-- 
cgit v1.2.3


From 0cd850265b567a53921da70bf6d07e4330500f34 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Sun, 8 Aug 2021 01:16:59 +0000
Subject: in the forum settings page, the "unlisted?" box is checked if the
 forum is already unlisted

---
 apioforum/forum.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/apioforum/forum.py b/apioforum/forum.py
index 108f0ba..084c75d 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -361,12 +361,14 @@ def forum_config_page(forum, create=False):
         if create:
             name = ""
             desc = ""
+            unlisted = False
         else:
             name = forum['name']
             desc = forum['description']
+            unlisted = forum['unlisted']
         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)
+                name=name,description=desc,unlisted=unlisted,cancel_link=cancel_link)
 
 @forum_route("edit",methods=["GET","POST"])
 @requires_bureaucrat
-- 
cgit v1.2.3


From 947c8168f1ce5df05fabc93975049b3ee49ad499 Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Mon, 9 Aug 2021 00:23:56 +0000
Subject: view forum permission

---
 apioforum/db.py                           |  3 +++
 apioforum/forum.py                        | 44 +++++++++++++++----------------
 apioforum/roles.py                        | 17 +++++++++---
 apioforum/templates/edit_forum.html       |  2 --
 apioforum/templates/edit_permissions.html |  2 ++
 apioforum/templates/role_assignment.html  |  4 +++
 6 files changed, 43 insertions(+), 29 deletions(-)

diff --git a/apioforum/db.py b/apioforum/db.py
index 5c3d2eb..c0c8c7e 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -204,6 +204,9 @@ 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 084c75d..2931df9 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -10,6 +10,7 @@ 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 functools
@@ -63,11 +64,11 @@ def forum_route(relative_path, **kwargs):
 
     return decorator
 
-def requires_permission(permission):
+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):
+            if not has_permission(forum['id'],g.user,permission,login_required):
                 abort(403)
             return f(forum, *args, **kwargs)
         return wrapper
@@ -75,6 +76,7 @@ def requires_permission(permission):
 
 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)
@@ -82,12 +84,8 @@ def requires_bureaucrat(f):
     return wrapper
 
 @forum_route("")
+@requires_permission("p_view_forum", login_required=False)
 def view_forum(forum):
-    # user should not be able to see anything about the forum if it is unlisted
-    # and the user does not have permission to see things
-    if forum['unlisted'] and not has_permission(forum['id'], g.user, "p_view_threads"):
-        abort(403)
-
     db = get_db()
     threads = db.execute(
         """SELECT
@@ -154,7 +152,8 @@ def view_forum(forum):
         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
@@ -174,6 +173,7 @@ def view_forum(forum):
 
 @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()
@@ -247,6 +247,7 @@ def edit_roles(forum):
             )
 
 @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) \
@@ -334,7 +335,6 @@ def forum_config_page(forum, create=False):
     if request.method == "POST":
         name = request.form["name"]
         desc = request.form["description"]
-        unlisted = "unlisted" in request.form
         if len(name) > 100 or len(name.strip()) == 0:
             flash("invalid name")
             return redirect(url_for('forum.edit_forum',forum_id=forum['id']))
@@ -342,14 +342,14 @@ def forum_config_page(forum, create=False):
             flash("invalid description")
             return redirect(url_for('forum.edit_forum',forum_id=forum['id']))
         if not create:
-            db.execute("UPDATE forums SET name = ?, description = ?, unlisted = ? WHERE id = ?",
-                    (name,desc,unlisted,forum['id']))
+            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,unlisted) VALUES (?,?,?,?)",
-                    (name,desc,forum['id'],unlisted))
+                "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 (?,?,?)",
@@ -361,14 +361,12 @@ def forum_config_page(forum, create=False):
         if create:
             name = ""
             desc = ""
-            unlisted = False
         else:
             name = forum['name']
             desc = forum['description']
-            unlisted = forum['unlisted']
         cancel_link = url_for('forum.view_forum',forum_id=forum['id'])
         return render_template("edit_forum.html",create=create,
-                name=name,description=desc,unlisted=unlisted,cancel_link=cancel_link)
+                name=name,description=desc,cancel_link=cancel_link)
 
 @forum_route("edit",methods=["GET","POST"])
 @requires_bureaucrat
@@ -380,13 +378,13 @@ def edit_forum(forum):
 def create_forum(forum):
     return forum_config_page(forum,create=True)
 
-@forum_route("unlisted")
-@requires_bureaucrat
-def view_unlisted(forum):
-    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)
+#@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():
diff --git a/apioforum/roles.py b/apioforum/roles.py
index d8e59ba..aa1d239 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -1,5 +1,6 @@
 
 from .db import get_db
+from .permissions import is_admin
 
 permissions = [
     "p_create_threads",
@@ -10,7 +11,8 @@ permissions = [
     "p_vote",
     "p_create_polls",
     "p_approve",
-    "p_create_subforum"
+    "p_create_subforum",
+    "p_view_forum"
 ]
 
 def get_role_config(forum_id, role):
@@ -76,9 +78,16 @@ def get_forum_roles(forum_id):
             """,(a['id'],)).fetchall()
     return set(r['role'] for r in configs)
 
-def has_permission(forum_id, user, permission, login_required=True):
-    if user == None and login_required: return False
-    role = get_user_role(forum_id, user) if user else "other"
+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]
diff --git a/apioforum/templates/edit_forum.html b/apioforum/templates/edit_forum.html
index 32bfaf1..f165676 100644
--- a/apioforum/templates/edit_forum.html
+++ b/apioforum/templates/edit_forum.html
@@ -17,8 +17,6 @@
 		maxlength="6000"
 		required
 	>{{description}}</textarea>
-	<input type="checkbox" id="unlisted" name="unlisted" {% if unlisted %}checked{% endif %}/>
-	<label for="unlisted">unlisted?</label>
 	<p>
 		<input type="submit" value="confirm">
 		<a href="{{cancel_link}}">cancel</a>
diff --git a/apioforum/templates/edit_permissions.html b/apioforum/templates/edit_permissions.html
index c92c9a9..59c9093 100644
--- a/apioforum/templates/edit_permissions.html
+++ b/apioforum/templates/edit_permissions.html
@@ -29,6 +29,8 @@
 			</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",
diff --git a/apioforum/templates/role_assignment.html b/apioforum/templates/role_assignment.html
index 74dc3cd..8309506 100644
--- a/apioforum/templates/role_assignment.html
+++ b/apioforum/templates/role_assignment.html
@@ -1,4 +1,5 @@
 {% 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>
@@ -12,6 +13,9 @@
 		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}}"/>
-- 
cgit v1.2.3


From 2d812c9c01bdc94e56500c0c0ffe187117e7696a Mon Sep 17 00:00:00 2001
From: citrons <citrons>
Date: Mon, 9 Aug 2021 20:00:42 +0000
Subject: fix bug in deleting threads without permission

---
 apioforum/thread.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apioforum/thread.py b/apioforum/thread.py
index 3c054d7..2fc9dca 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -250,7 +250,7 @@ def delete_thread(thread_id):
         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=post["thread"]))
+        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,))
-- 
cgit v1.2.3