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