aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorraven <citrons@mondecitronne.com>2026-11-01 23:29:08 -0600
committerraven <citrons@mondecitronne.com>2026-11-01 23:51:01 -0600
commit53590c7544f15d5b8afa3e5e27e47b51a6ed7904 (patch)
tree0f9b42bab7d1f6542eec56ba0e82beb170c8704e
parent4bbcf7390f62263b0faad9dabfb8707147717283 (diff)
add support for "wiki page" thread type
a thread has a type, which may be "thread" or "wiki page." if the type is wiki page, then its first post becomes editable by other people. additionally, each edit is now tracked in the database.
-rw-r--r--apioforum/db.py25
-rw-r--r--apioforum/forum.py23
-rw-r--r--apioforum/roles.py1
-rw-r--r--apioforum/static/style.css4
-rw-r--r--apioforum/templates/common.html29
-rw-r--r--apioforum/templates/config_thread.html19
-rw-r--r--apioforum/templates/edit_permissions.html2
-rw-r--r--apioforum/templates/edit_post.html8
-rw-r--r--apioforum/templates/view_thread.html80
-rw-r--r--apioforum/thread.py33
10 files changed, 165 insertions, 59 deletions
diff --git a/apioforum/db.py b/apioforum/db.py
index ead4e5a..9dcf1ee 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -261,6 +261,31 @@ ALTER TABLE users ADD COLUMN banreason TEXT;
"""
ALTER TABLE threads ADD COLUMN locked INT NOT NULL DEFAULT 0;
""",
+"""
+ALTER TABLE threads ADD COLUMN type TEXT NOT NULL DEFAULT "thread";
+ALTER TABLE posts ADD COLUMN wiki INT NOT NULL DEFAULT 0;
+ALTER TABLE posts ADD COLUMN updated_by_others INT NOT NULL DEFAULT 0;
+CREATE TABLE revisions (
+ post NOT NULL REFERENCES posts(id),
+ id INTEGER PRIMARY KEY,
+ author NOT NULL REFERENCES users(username),
+ content TEXT NOT NULL,
+ time TIMESTAMP,
+ comment TEXT
+);
+CREATE TABLE wiki_pages (
+ path TEXT NOT NULL,
+ forum NOT NULL REFERENCES forums(id),
+ thread REFERENCES threads(id),
+ type TEXT NOT NULL DEFAULT "page",
+ PRIMARY KEY (path, forum)
+);
+ALTER TABLE role_config ADD COLUMN p_edit_wiki INT NOT NULL DEFAULT 1
+""",
+"""
+INSERT INTO revisions (post, author, content, time)
+ SELECT id, author, content, updated FROM posts;
+""",
]
diff --git a/apioforum/forum.py b/apioforum/forum.py
index a955ed5..b731a24 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -280,9 +280,17 @@ def create_thread(forum):
if forum is None:
flash("that forum doesn't exist")
return redirect(url_for('index'))
+
+ if "type" in request.args:
+ thread_type = request.args["type"]
+ else:
+ thread_type = "thread"
+ thread_type = thread_type.replace('_', " ")
+ if thread_type != "thread" and thread_type != "wiki page":
+ abort(404)
if g.user is None:
- flash("you need to be logged in to create a thread")
+ flash("you need to be logged in to create a " + thread_type)
return redirect(url_for('index'))
if request.method == "POST":
@@ -298,15 +306,21 @@ def create_thread(forum):
lock = int(request.form.get('lock','0'))
cur = db.cursor()
cur.execute(
- """INSERT INTO threads (title,creator,created,updated,forum,locked)
- VALUES (?,?,current_timestamp,current_timestamp,?,?);""",
- (title,g.user,forum['id'],lock)
+ """INSERT INTO threads (title,creator,created,updated,forum,locked,type)
+ VALUES (?,?,current_timestamp,current_timestamp,?,?,?);""",
+ (title,g.user,forum['id'],lock,thread_type)
)
thread_id = cur.lastrowid
cur.execute(
"INSERT INTO posts (thread,created,author,content) VALUES (?,current_timestamp,?,?);",
(thread_id,g.user,content)
)
+ db.execute("""
+ INSERT INTO revisions (post,author,content,time,comment)
+ VALUES (?,?,?,current_timestamp,?);
+ """, (cur.lastrowid,g.user,content,"(created)"))
+ if thread_type == "wiki page":
+ db.execute("UPDATE posts SET wiki = 1 WHERE id = ?;", (cur.lastrowid,))
thread = {'id': thread_id, 'forum': forum['id']}
apply_selected_tags(thread, request.form)
db.commit()
@@ -321,6 +335,7 @@ def create_thread(forum):
return render_template("config_thread.html",
forum=forum,
+ thread_type=thread_type,
avail_tags=get_avail_tags(forum['id']))
@forum_route("roles",methods=("GET","POST"))
diff --git a/apioforum/roles.py b/apioforum/roles.py
index 42ce21c..e2428af 100644
--- a/apioforum/roles.py
+++ b/apioforum/roles.py
@@ -10,6 +10,7 @@ permissions = [
"p_view_threads",
"p_vote",
"p_create_polls",
+ "p_edit_wiki",
"p_approve",
"p_create_subforum",
"p_view_forum"
diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index 573411c..8a30355 100644
--- a/apioforum/static/style.css
+++ b/apioforum/static/style.css
@@ -527,3 +527,7 @@ sitelen {
src: local(nasin-nanpa), url("nasin-nanpa.otf") format("opentype");
}
+.wiki-body {
+ margin-bottom: 5px;
+}
+
diff --git a/apioforum/templates/common.html b/apioforum/templates/common.html
index 20bc00b..1177405 100644
--- a/apioforum/templates/common.html
+++ b/apioforum/templates/common.html
@@ -20,12 +20,13 @@
<span class="post-heading-a">
{{disp_user(post.author)}}
- {% set role = get_user_role(forum, post.author) %}
- {% if role != "other" %}
- <span class="user-role">
- ({{ role }})
- </span>
- {% endif %}
+ {%- set role = get_user_role(forum, post.author) -%}
+ {%- if role != "other" %}
+ <span class="user-role"> ({{ role }})</span>
+ {%- endif -%}
+ {%- if post.updated_by_others == 1 -%}
+ , <i>et al.</i>
+ {%- endif %}
{{ts(post.created)}}
@@ -35,7 +36,11 @@
</span>
<span class="post-heading-b">
{% if buttons %}
- {% if post.author == g.user %}
+ {% if post.author == g.user or (
+ post.wiki == 1 and
+ has_permission(forum, g.user, "p_edit_wiki")
+ and not thread_info(post.thread).locked == 1)
+ %}
<a class="actionbutton"
href="{{url_for('thread.edit_post',post_id=post.id)}}">edit</a>
{% endif %}
@@ -173,12 +178,12 @@
</svg>
{% endmacro %}
-{% macro pagination_nav(page,max_pageno,view,bottom=False) %}
+{% macro pagination_nav(page,max_pageno,view,bottom=False,fragment="") %}
<nav aria-label="pagination" class="pages{% if not bottom %} pages-top{% endif %}">
<div class="links">
{% if page > 1 %}
- <a href="{{url_for(view,**kwargs)}}" aria-label="first page">&lt;&lt; first</a>
- <a href="{{url_for(view,page=page-1,**kwargs)}}" aria-label="previous page">&lt; prev</a>
+ <a href="{{url_for(view,**kwargs)}}#{{fragment}}" aria-label="first page">&lt;&lt; first</a>
+ <a href="{{url_for(view,page=page-1,**kwargs)}}#{{fragment}}" aria-label="previous page">&lt; prev</a>
{% else %}
<a aria-label="first page">&lt;&lt; first</a>
<a aria-label="previous page">&lt; prev</a>
@@ -187,8 +192,8 @@
<div class="pageno"><span id="the-word-page">page </span>{{page}} of {{max_pageno}}</div>
<div class="links">
{% if page < max_pageno %}
- <a href="{{url_for(view,page=page+1,**kwargs)}}" aria-label="next page">next &gt;</a>
- <a href="{{url_for(view,page=max_pageno,**kwargs)}}" aria-label="last page">last &gt;&gt;</a>
+ <a href="{{url_for(view,page=page+1,**kwargs)}}#{{fragment}}" aria-label="next page">next &gt;</a>
+ <a href="{{url_for(view,page=max_pageno,**kwargs)}}#{{fragment}}" aria-label="last page">last &gt;&gt;</a>
{% else %}
<a aria-label="next page">next &gt;</a>
<a aria-label="last page">last &gt;&gt;</a>
diff --git a/apioforum/templates/config_thread.html b/apioforum/templates/config_thread.html
index aa4575f..a97e8e6 100644
--- a/apioforum/templates/config_thread.html
+++ b/apioforum/templates/config_thread.html
@@ -1,24 +1,27 @@
{% extends 'base.html' %}
{% from 'common.html' import tag %}
{% block header %}
+
<h1>
{% block title %}
- {% if thread %}configure thread '{{thread.title}}'
- {% else %}create new thread{% endif %}
+ {% set type = thread_type or thread.type %}
+ {% if thread %}configure {{type}} '{{thread.title}}'
+ {% else %}create new {{type}}{% endif %}
{% endblock %}
</h1>
{% endblock %}
{% block content %}
+{% set type = thread_type or thread.type %}
<form method="post">
<fieldset>
-<legend>thread</legend>
-<label for="title">thread title</label>
+<legend>{{type}}</legend>
+<label for="title">{{type}} title</label>
<input type="text" id="title" name="title" {%if thread%}value="{{thread.title}}"{%endif%}>
{% if not thread %}
<br>
- <label for="content">thread content</label>
- <textarea name="content" id="content" class="new-post-box" placeholder="thread content here"></textarea>
+ <label for="content">{{type}} content</label>
+ <textarea name="content" id="content" class="new-post-box" placeholder="welcome to my extraordinary {{type}}."></textarea>
{% endif %}
</fieldset>
@@ -54,7 +57,7 @@
<input type="submit" value="confirm">
<a href="{{url_for('thread.view_thread',thread_id=thread.id)}}">cancel</a>
{% else %}
- <input type="submit" value="create thread">
+ <input type="submit" value="create {{type}}">
<a href="{{url_for('forum.view_forum',forum_id=forum.id)}}">cancel</a>
{% endif %}
</form>
@@ -76,7 +79,7 @@
</form>
{% else %}
<h2>delete poll</h2>
-<p>there is already a poll attached to this thread. you can delete it, which will allow you to create a new one, but this will erase all existing votes and data for the current poll.</p>
+<p>there is already a poll attached to this {{type}}. you can delete it, which will allow you to create a new one, but this will erase all existing votes and data for the current poll.</p>
<form action="{{url_for('thread.delete_poll',thread_id=thread.id)}}" method="post">
<input type="submit" value="confirm: delete poll">
</form>
diff --git a/apioforum/templates/edit_permissions.html b/apioforum/templates/edit_permissions.html
index 59c9093..3903048 100644
--- a/apioforum/templates/edit_permissions.html
+++ b/apioforum/templates/edit_permissions.html
@@ -45,6 +45,8 @@
"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_edit_wiki","edit wiki",
+ "edit the content of wiki pages")}}
{{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/edit_post.html b/apioforum/templates/edit_post.html
index 275f97f..573fd4e 100644
--- a/apioforum/templates/edit_post.html
+++ b/apioforum/templates/edit_post.html
@@ -1,12 +1,18 @@
{% from 'common.html' import disp_post %}
{% extends 'base.html' %}
{% block header %}
-<h1>{%block title%}editing post{%endblock%}</h1>
+<h1>{%block title%}editing {% if post.wiki == 1 %}wiki{% endif %} post{%endblock%}</h1>
{% endblock %}
{% block content %}
<form method="post">
<textarea class="new-post-box" name="newcontent">{{post.content}}</textarea>
+{% if post.wiki == 1 %}
+<p>
+<label for="comment">comment</label>
+<input type="text" id="comment" name="comment" placeholder="fixed spelling issues">
+</p>
+{% endif %}
<p>confirm edit?</p>
<input type="submit" value="confirm">
<a href="{{post_jump(post.id)}}">cancel</a>
diff --git a/apioforum/templates/view_thread.html b/apioforum/templates/view_thread.html
index 1a74f60..e5d69c7 100644
--- a/apioforum/templates/view_thread.html
+++ b/apioforum/templates/view_thread.html
@@ -18,10 +18,10 @@
<div class="thread-top-bar">
<span class="thread-top-bar-a">
{% 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>
+ <a class="actionbutton" href="{{url_for('thread.config_thread',thread_id=thread.id)}}">configure {{thread.type}}</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>
+ <a class="actionbutton" href="{{url_for('thread.delete_thread',thread_id=thread.id)}}">delete {{thread.type}}</a>
{% endif %}
</span>
&nbsp;
@@ -32,53 +32,68 @@
</span>
</div>
-{{ pagination_nav(page,max_pageno,'thread.view_thread',thread_id=thread.id) }}
-<div class="posts">
- {% for post in posts %}
- {% if votes[post.id] %}
+{% macro show_post(post) %}
+ {% if votes[post.id] %}
- {% set vote = votes[post.id] %}
- {% set option_idx = vote.option_idx %}
-
- {# this is bad but it's going to get refactored anyway #}
- {% set footer %}
- {% if vote.is_retraction %}
- {% if not post.deleted %}
- {{post.author}} retracted their vote
- {% else %}
- this post retracted a vote
- {% endif %}
+ {% set vote = votes[post.id] %}
+ {% set option_idx = vote.option_idx %}
+
+ {# this is bad but it's going to get refactored anyway #}
+ {% set footer %}
+ {% if vote.is_retraction %}
+ {% if not post.deleted %}
+ {{post.author}} retracted their vote
{% else %}
- {% set option = poll.options[option_idx-1] %}
- {% if vote.current %}
- {{post.author}} votes for {{option_idx}}: {{option.text}}
+ 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 %}
+ {% if not post.deleted %}
+ {{post.author}} voted for {{option_idx}}: {{option.text}}, but later changed their vote
{% else %}
- {% 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 %}
+ this post presented a vote that was later changed
{% endif %}
{% endif %}
+ {% endif %}
- {% endset %}
+ {% endset %}
- {{ disp_post(post, forum=thread.forum, buttons=True, footer=footer) }}
-
- {% else %}
- {{ disp_post(post, forum=thread.forum, buttons=True) }}
+ {{ disp_post(post, forum=thread.forum, buttons=True, footer=footer) }}
+
+ {% else %}
+ {{ disp_post(post, forum=thread.forum, buttons=True) }}
+ {% endif %}
+{% endmacro %}
+
+{% if wiki_body %}
+<div class="wiki-body">
+ {{show_post(wiki_body)}}
+</div>
+{% endif %}
+
+<div id="replies">
+{% if not wiki_body or page != 1 or posts|length > 1 %}
+ {{ pagination_nav(page,max_pageno,'thread.view_thread',thread_id=thread.id,fragment="replies") }}
+{% endif %}
+<div class="posts">
+ {% for post in posts %}
+ {% if post != wiki_body %}
+ {{show_post(post)}}
{% endif %}
{% endfor %}
</div>
{% if page != max_pageno %}
- {{ pagination_nav(page,max_pageno,'thread.view_thread',thread_id=thread.id,bottom=True) }}
+ {{ pagination_nav(page,max_pageno,'thread.view_thread',thread_id=thread.id,bottom=True,fragment="replies") }}
{% elif g.user and has_permission(thread.forum, g.user, "p_reply_threads") %}
{% if thread.locked and not has_permission(thread.forum, g.user, "p_manage_threads") %}
<p>this thread is locked, and may not recieve further replies</p>
{% else %}
<form class="new-post" action="{{url_for('thread.create_post',thread_id=thread.id)}}" method="POST">
{% if thread.locked %}<p>this thread is locked, but you can reply to it anyway</p>{% endif %}
- <textarea class="new-post-box" placeholder="your post here..." name="content"></textarea>
+ <textarea class="new-post-box" placeholder="your reply here..." name="content"></textarea>
{% if poll and has_permission(thread.forum, g.user, "p_vote") %}
<fieldset>
<legend>poll: {{poll.title}}</legend>
@@ -109,4 +124,5 @@
{% else %}
<p>please log in to reply to this thread</p>
{% endif %}
+</div>
{% endblock %}
diff --git a/apioforum/thread.py b/apioforum/thread.py
index 3c4a3a3..8911e95 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -116,6 +116,14 @@ def view_thread(thread_id,page=1):
db.execute("DELETE FROM read WHERE user = ? AND thread = ?;", (g.user, thread_id))
db.execute("INSERT INTO read (user,thread,time) VALUES (?,?,current_timestamp);", (g.user, thread_id))
db.commit()
+
+ wiki_body = None
+ if thread['type'] == "wiki page":
+ wiki_body = db.execute("""
+ SELECT * FROM posts
+ WHERE posts.thread = ?
+ ORDER BY created ASC;
+ """,(thread_id,)).fetchone()
return render_template(
"view_thread.html",
@@ -125,6 +133,7 @@ def view_thread(thread_id,page=1):
poll=poll,
votes=votes,
has_voted=has_voted,
+ wiki_body=wiki_body,
page=page,
max_pageno=max_pageno,
)
@@ -266,6 +275,10 @@ def create_post(thread_id):
VALUES (?,?,?,current_timestamp,?);
""",(thread_id,g.user,content,vote_id))
post_id = cur.lastrowid
+ cur.execute("""
+ INSERT INTO revisions (post,author,content,time,comment)
+ VALUES (?,?,?,current_timestamp,?);
+ """, (post_id,g.user,content,"(created)"))
cur.execute(
"UPDATE threads SET updated = current_timestamp WHERE id = ?;",
(thread_id,)
@@ -330,23 +343,39 @@ def delete_thread(thread_id):
def edit_post(post_id):
db = get_db()
post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone()
+ thread = thread_info(post['thread'])
if post is None:
flash("that post doesn't exist")
return redirect(url_for('index'))
jump_url = post_jump(post_id)
if post['author'] != g.user:
- flash("you can only edit posts that you created")
- return redirect(jump_url)
+ if post['wiki'] != 1:
+ flash("you can only edit posts that you created")
+ return redirect(jump_url)
+ elif thread['locked'] == 1:
+ flash("you cannot edit this post because the page is locked")
+ return redirect(jump_url)
+ if not has_permission(thread['forum'], g.user, "p_edit_wiki"):
+ flash("you do not have permission to do that")
+ return redirect(jump_url)
if request.method == "POST":
err = None
newcontent = request.form['newcontent']
+ comment = request.form['comment'] if 'comment' in request.form else None
+
if len(newcontent.strip()) == 0:
err="post contents can't be empty"
if err is None:
db.execute(
"UPDATE posts SET content = ?, edited = 1, updated = current_timestamp WHERE id = ?",(newcontent,post_id))
+ db.execute("""
+ INSERT INTO revisions (post, author, content, time, comment)
+ VALUES (?,?,?,current_timestamp,?);
+ """, (post_id,g.user,newcontent,comment))
+ if g.user != post['author']:
+ db.execute("UPDATE posts SET updated_by_others = 1 WHERE id = ?",(post_id,))
db.commit()
flash("post edited editiously")
return redirect(jump_url)