aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--apioforum/__init__.py2
-rw-r--r--apioforum/db.py37
-rw-r--r--apioforum/forum.py76
-rw-r--r--apioforum/static/style.css57
-rw-r--r--apioforum/templates/base.html2
-rw-r--r--apioforum/templates/common.html27
-rw-r--r--apioforum/templates/create_thread.html2
-rw-r--r--apioforum/templates/view_forum.html64
-rw-r--r--apioforum/templates/view_thread.html3
9 files changed, 204 insertions, 66 deletions
diff --git a/apioforum/__init__.py b/apioforum/__init__.py
index 9d49f36..30dd813 100644
--- a/apioforum/__init__.py
+++ b/apioforum/__init__.py
@@ -47,6 +47,8 @@ def create_app():
p += "?" + request.query_string.decode("utf-8")
return dict(path_for_next=p)
+ app.jinja_env.globals.update(forum_path=forum.forum_path)
+
from .mdrender import render
@app.template_filter('md')
def md_render(s):
diff --git a/apioforum/db.py b/apioforum/db.py
index 25bda94..97bd0e2 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -115,7 +115,42 @@ ALTER TABLE posts ADD COLUMN vote INTEGER REFERENCES votes(id);
CREATE VIEW vote_counts AS
SELECT poll, option_idx, count(*) AS num FROM votes WHERE current GROUP BY option_idx,poll;
""",
-
+"""
+CREATE TABLE forums (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL,
+ parent INTEGER REFERENCES forums(id),
+ description TEXT
+);
+INSERT INTO forums (name,parent,description) values ('apioforum',null,
+ 'welcome to the apioforum\n\n' ||
+ 'forum rules: do not be a bad person. do not do bad things.');
+
+PRAGMA foreign_keys = off;
+BEGIN TRANSACTION;
+CREATE TABLE threads_new (
+ id INTEGER PRIMARY KEY,
+ title TEXT NOT NULL,
+ creator TEXT NOT NULL,
+ created TIMESTAMP NOT NULL,
+ updated TIMESTAMP NOT NULL,
+ forum NOT NULL REFERENCES forums(id),
+ poll INTEGER REFERENCES polls(id)
+);
+INSERT INTO threads_new (id,title,creator,created,updated,forum)
+ SELECT id,title,creator,created,updated,1 FROM threads;
+DROP TABLE threads;
+ALTER TABLE threads_new RENAME TO threads;
+COMMIT;
+PRAGMA foreign_keys = on;
+""",
+"""
+CREATE VIEW most_recent_posts AS
+ SELECT max(id), * FROM posts GROUP BY thread;
+
+CREATE VIEW number_of_posts AS
+ SELECT thread, count(*) AS num_replies FROM posts GROUP BY thread;
+""",
]
def init_db():
diff --git a/apioforum/forum.py b/apioforum/forum.py
index fa6fcc8..1d26f3a 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -10,23 +10,45 @@ from .db import get_db
from .mdrender import render
from sqlite3 import OperationalError
+import datetime
bp = Blueprint("forum", __name__, url_prefix="/")
-
@bp.route("/")
-def view_forum():
+def not_actual_index():
+ return redirect("/1")
+
+def forum_path(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()
+ ancestors.reverse()
+ return ancestors
+
+@bp.route("/<int:forum_id>")
+def view_forum(forum_id):
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,
- threads.updated, count(posts.id) as num_replies, max(posts.id), posts.author as last_user
+ """SELECT
+ threads.id, threads.title, threads.creator, threads.created,
+ threads.updated, number_of_posts.num_replies,
+ 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
FROM threads
- INNER JOIN posts ON posts.thread = threads.id
- GROUP BY threads.id
+ 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;
- """).fetchall()
+ """,(forum_id,)).fetchall()
thread_tags = {}
- preview_post = {}
#todo: somehow optimise this
for thread in threads:
thread_tags[thread['id']] = db.execute(
@@ -35,19 +57,37 @@ def view_forum():
WHERE thread_tags.thread = ?
ORDER BY tags.id;
""",(thread['id'],)).fetchall()
- preview_post[thread['id']] = db.execute(
- """SELECT * FROM posts WHERE thread = ?
- ORDER BY created DESC;
- """,(thread['id'],)).fetchone()
+
+ subforums_rows = db.execute("""
+ SELECT max(threads.updated) as updated, forums.* FROM forums
+ LEFT OUTER JOIN threads ON threads.forum=forums.id
+ WHERE parent = ?
+ GROUP BY forums.id
+ ORDER BY name ASC
+ """,(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)
+
+
return render_template("view_forum.html",
+ forum=forum,
+ subforums=subforums,
threads=threads,
thread_tags=thread_tags,
- preview_post=preview_post
)
-@bp.route("/create_thread",methods=("GET","POST"))
-def create_thread():
+@bp.route("/<int:forum_id>/create_thread",methods=("GET","POST"))
+def create_thread(forum_id):
db = get_db()
+ 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'))
if g.user is None:
flash("you need to be logged in to create a thread")
@@ -63,8 +103,8 @@ def create_thread():
if err is None:
cur = db.cursor()
cur.execute(
- "INSERT INTO threads (title,creator,created,updated) VALUES (?,?,current_timestamp,current_timestamp);",
- (title,g.user)
+ "INSERT INTO threads (title,creator,created,updated,forum) VALUES (?,?,current_timestamp,current_timestamp,?);",
+ (title,g.user,forum_id)
)
thread_id = cur.lastrowid
cur.execute(
@@ -96,7 +136,7 @@ def search():
""", (query,)).fetchall()
except OperationalError:
flash('your search query was malformed.')
- return redirect(url_for("forum.view_forum"))
+ return redirect(url_for("forum.not_actual_index"))
display_thread_id = [ True ] * len(results)
last_thread = None
diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index 6178dd9..62215c7 100644
--- a/apioforum/static/style.css
+++ b/apioforum/static/style.css
@@ -81,35 +81,36 @@ dt { font-weight: bold }
img { max-width: 100% }
-nav { float: right; padding: 5px; margin: 2px; border: 1px solid black; display:flex; align-items: center; flex-wrap: wrap }
-nav p { margin-left: 15px; margin-top: 0; margin-bottom: 0; margin-right: 10px; padding: 0 }
-nav p:first-of-type { margin-left:0.5em }
-nav a { color: blue; text-decoration: none }
-nav .links { display: flex; }
+nav#navbar { float: right; padding: 5px; margin: 2px; 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 }
+nav#navbar .links { display: flex; }
/* todo: make the navbar less bad */
.flashmsg { border: 1px solid black; background-color: yellow; max-width: max-content; padding: 5px; clear: both;}
-.thread-listing:nth-child(even) { background-color: var(--alternating-colour-even) }
-.thread-listing:nth-child(odd) { background-color: var(--alternating-colour-odd) }
+.listing:nth-child(even) { background-color: var(--alternating-colour-even) }
+.listing:nth-child(odd) { background-color: var(--alternating-colour-odd) }
-.thread-listing {
+.listing {
border-left: 1px solid black;
border-right: 1px solid black;
border-top: 1px solid black;
padding: 10px;
}
-.thread-listing:last-of-type { border-bottom: 1px solid black; }
-.thread-listing-main {
+.listing:last-of-type { border-bottom: 1px solid black; }
+.listing-main {
display: flex;
align-items: center;
}
-.thread-listing-title {
+.listing-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
flex-grow: 1;
}
+
.thread-listing-tags {
display: flex;
align-items: center;
@@ -123,7 +124,7 @@ nav .links { display: flex; }
flex-wrap: nowrap;
}
.thread-listing-creator { margin-right: 5px; }
-.thread-preview {
+.listing-caption {
overflow: hidden;
font-size: smaller;
white-space: nowrap;
@@ -139,7 +140,7 @@ nav .links { display: flex; }
/* wide screens */
@media all and (min-width: 600px) {
- .thread-listing-title { font-size: larger; }
+ .listing-title { font-size: larger; }
}
/* small screens */
@@ -153,22 +154,6 @@ nav .links { display: flex; }
}
}
-
-.threadlisting-part {
- background-color: inherit;
- display: table-cell;
- vertical-align: middle;
- padding: 3px;
- text-align: center;
-}
-
-.threadlisting-part-title {
- text-overflow: ellipsis;
- overflow: hidden;
-}
-
-.threadlisting-header { font-weight: bold }
-
.actionbutton::before {
content: "[";
color: grey;
@@ -216,3 +201,17 @@ blockquote {
border: 1px solid grey;
padding: 4px;
}
+
+.breadcrumbs {
+ list-style: none;
+}
+
+.breadcrumbs li {
+ display: inline;
+}
+
+.breadcrumbs li+li::before {
+ content: "/\00a0";
+ padding: 8px;
+}
+
diff --git a/apioforum/templates/base.html b/apioforum/templates/base.html
index 33e49f6..9cfd7f3 100644
--- a/apioforum/templates/base.html
+++ b/apioforum/templates/base.html
@@ -10,7 +10,7 @@
<link rel="icon" href="//gh0.pw/favicon.ico">
</head>
<body>
- <nav>
+ <nav id="navbar">
<p style="font-family: monospace;"><b>apio</b><i>forum</i>&trade;</p>
<form class="search-form" action="/search">
<input type="search" placeholder="query" name="q">
diff --git a/apioforum/templates/common.html b/apioforum/templates/common.html
index 3321085..44cfbce 100644
--- a/apioforum/templates/common.html
+++ b/apioforum/templates/common.html
@@ -49,3 +49,30 @@
{% macro tag(the_tag) -%}
<span class="tag" style="color: {{the_tag.text_colour}}; background-color: {{the_tag.bg_colour}}">{{the_tag.name}}</span>
{%- endmacro %}
+
+{% macro breadcrumb() %}
+<nav aria-label="Breadcrumb">
+<ol class="breadcrumbs">
+ {{- caller() -}}
+</ol>
+</nav>
+{% endmacro %}
+
+{% macro forum_bc_entries(forum_id) -%}
+ {%- for f in forum_path(forum_id) -%}
+ <li><a href="{{url_for('forum.view_forum',forum_id=f.id)}}">{{ f.name }}</a></li>
+ {%- endfor %}
+{%- endmacro %}
+
+{% macro forum_breadcrumb(forum) %}
+ {%- call breadcrumb() %}
+ {{ forum_bc_entries(forum.id) }}
+ {% endcall -%}
+{% endmacro %}
+
+{% macro thread_breadcrumb(thread) %}
+ {%- call breadcrumb() %}
+ {{ forum_bc_entries(thread.forum) }}
+ <li>{{ thread.title }}</li>
+ {% endcall -%}
+{% endmacro %}
diff --git a/apioforum/templates/create_thread.html b/apioforum/templates/create_thread.html
index 9de7312..04b4f42 100644
--- a/apioforum/templates/create_thread.html
+++ b/apioforum/templates/create_thread.html
@@ -4,7 +4,7 @@
{% endblock %}
{% block content %}
-<form action="{{url_for('forum.create_thread')}}", method="POST">
+<form method="POST">
<label for="title">thread title</label>
<input name="title" id="title">
<br>
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index b27088d..d075d85 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -1,19 +1,52 @@
{% extends 'base.html' %}
-{% from 'common.html' import ts, tag, disp_user, post_url %}
-{% block header %}<h1>{% block title %}apioforum{%endblock%}</h1>{%endblock%}
+{% from 'common.html' import ts, tag, disp_user, post_url, forum_breadcrumb %}
+{% block header %}
+<h1>{% block title %}{{forum.name}}{%endblock%}</h1>
+{% if forum.id != 1 %}
+ {{ forum_breadcrumb(forum) }}
+{% endif %}
+{%endblock%}
+
{%block content%}
-<p>welcome to the apioforum</p>
-<p>forum rules: do not be a bad person. do not do bad things.</p>
+{% if forum.description %}
+{{forum.description|md|safe}}
+{% endif %}
+
+{% if subforums %}
+<h2>subforae</h2>
+<div class="forum-list">
+ {% for subforum in subforums %}
+ <div class="listing">
+ <div class="listing-main">
+ <div class="listing-title">
+ <a href="{{url_for('forum.view_forum',forum_id=subforum.id)}}">
+ {{- subforum.name -}}
+ </a>
+ </div>
+ </div>
+ <div class="listing-caption">
+ {% if subforum.updated %}
+ last activity {{ts(subforum.updated)}} ago
+ {% else %}
+ no threads
+ {% endif %}
+ </div>
+ </div>
+ {% endfor %}
+</div>
+{% endif %}
+
+<h2>threads</h2>
{% if g.user %}
-<p><a class="actionbutton" href="{{url_for('forum.create_thread')}}">create new thread</a></p>
+<p><a class="actionbutton" href="{{url_for('forum.create_thread',forum_id=forum.id)}}">create new thread</a></p>
{% else %}
<p>please log in to create a new thread</p>
{% endif %}
<div class="thread-list">
{%for thread in threads%}
- <div class="thread-listing">
- <div class="thread-listing-main">
- <div class="thread-listing-title">
+ <div class="listing">
+ <div class="listing-main">
+ <div class="listing-title">
<a href="{{url_for('thread.view_thread',thread_id=thread.id)}}">
{{- thread.title -}}
</a>
@@ -30,20 +63,21 @@
{{ ts(thread.created) }}
</div>
</div>
- {% if preview_post[thread.id] %}
- <div class="thread-preview">
- {{ disp_user(preview_post[thread.id].author) }}
+ {#{% if thread.mrp_id %}#}
+ <div class="listing-caption">
+ {{ disp_user(thread.mrp_author) }}
<span class="thread-preview-ts">
- {{ ts(preview_post[thread.id].created) }}
+ {{ ts(thread.mrp_created) }}
</span>
<span class="thread-preview-post">
- <a href="{{post_url(preview_post[thread.id])}}">
- {{ preview_post[thread.id].content[:500]|e }}
+ <a href="{{url_for('thread.view_thread',thread_id=thread.id)}}#post_{{thread.mrp_id}}">
+ {{ thread.mrp_content[:500]|e }}
</a>
</span>
</div>
- {% endif %}
+ {#{% endif %}#}
</div>
{%endfor%}
</div>
+</main>
{%endblock%}
diff --git a/apioforum/templates/view_thread.html b/apioforum/templates/view_thread.html
index 7bf253d..d4a43ef 100644
--- a/apioforum/templates/view_thread.html
+++ b/apioforum/templates/view_thread.html
@@ -1,7 +1,8 @@
-{% from 'common.html' import disp_post,tag %}
+{% from 'common.html' import disp_post,tag,thread_breadcrumb %}
{% extends 'base.html' %}
{% block header %}
<h1>{%block title %}{{thread.title}}{% endblock %}</h1>
+{{ thread_breadcrumb(thread) }}
{% endblock %}
{%block content%}