summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorcitrons <citrons>2021-08-06 03:31:07 +0000
committercitrons <citrons>2021-08-06 03:31:07 +0000
commit5cf3eaebb1db20d61b88f044dfb2a34512aecd61 (patch)
tree945a270e59bb6d691e6f20686f2f9c8ec3d6e8b4
parent52c63cddb3f7860862af6a2185a728baf7593cc7 (diff)
parent74a992ca018a69cc1de6225a681ca17c19c74ffa (diff)
merge the things; poll permissions
-rw-r--r--apioforum/__init__.py6
-rw-r--r--apioforum/csscolors.py150
-rw-r--r--apioforum/db.py60
-rw-r--r--apioforum/forum.py48
-rw-r--r--apioforum/mdrender.py27
-rw-r--r--apioforum/static/md-colors.css306
-rw-r--r--apioforum/static/style.css37
-rw-r--r--apioforum/templates/base.html3
-rw-r--r--apioforum/templates/common.html49
-rw-r--r--apioforum/templates/config_thread.html25
-rw-r--r--apioforum/templates/view_forum.html43
-rw-r--r--apioforum/templates/view_post.html12
-rw-r--r--apioforum/templates/view_thread.html71
-rw-r--r--apioforum/thread.py198
-rw-r--r--apioforum/util.py16
-rw-r--r--setup.py1
16 files changed, 1006 insertions, 46 deletions
diff --git a/apioforum/__init__.py b/apioforum/__init__.py
index 1d96d8c..087df81 100644
--- a/apioforum/__init__.py
+++ b/apioforum/__init__.py
@@ -17,6 +17,9 @@ def create_app():
except OSError:
pass
+ app.jinja_env.trim_blocks = True
+ app.jinja_env.lstrip_blocks = True
+
from . import db
db.init_app(app)
from . import permissions
@@ -40,6 +43,9 @@ def create_app():
from .fuzzy import fuzzy
app.jinja_env.filters['fuzzy']=fuzzy
+ from .util import gen_colour
+ app.jinja_env.filters['gen_colour']=gen_colour
+
@app.context_processor
def path_for_next():
p = request.path
diff --git a/apioforum/csscolors.py b/apioforum/csscolors.py
new file mode 100644
index 0000000..25bdfdd
--- /dev/null
+++ b/apioforum/csscolors.py
@@ -0,0 +1,150 @@
+csscolors = [
+ "black",
+ "silver",
+ "gray",
+ "white",
+ "maroon",
+ "red",
+ "purple",
+ "fuchsia",
+ "green",
+ "lime",
+ "olive",
+ "yellow",
+ "navy",
+ "blue",
+ "teal",
+ "aqua",
+ "orange",
+ "aliceblue",
+ "antiquewhite",
+ "aquamarine",
+ "azure",
+ "beige",
+ "bisque",
+ "blanchedalmond",
+ "blueviolet",
+ "brown",
+ "burlywood",
+ "cadetblue",
+ "chartreuse",
+ "chocolate",
+ "coral",
+ "cornflowerblue",
+ "cornsilk",
+ "crimson",
+ "cyan",
+ "darkblue",
+ "darkcyan",
+ "darkgoldenrod",
+ "darkgray",
+ "darkgreen",
+ "darkgrey",
+ "darkkhaki",
+ "darkmagenta",
+ "darkolivegreen",
+ "darkorange",
+ "darkorchid",
+ "darkred",
+ "darksalmon",
+ "darkseagreen",
+ "darkslateblue",
+ "darkslategray",
+ "darkslategrey",
+ "darkturquoise",
+ "darkviolet",
+ "deeppink",
+ "deepskyblue",
+ "dimgray",
+ "dimgrey",
+ "dodgerblue",
+ "firebrick",
+ "floralwhite",
+ "forestgreen",
+ "gainsboro",
+ "ghostwhite",
+ "gold",
+ "goldenrod",
+ "greenyellow",
+ "grey",
+ "honeydew",
+ "hotpink",
+ "indianred",
+ "indigo",
+ "ivory",
+ "khaki",
+ "lavender",
+ "lavenderblush",
+ "lawngreen",
+ "lemonchiffon",
+ "lightblue",
+ "lightcoral",
+ "lightcyan",
+ "lightgoldenrodyellow",
+ "lightgray",
+ "lightgreen",
+ "lightgrey",
+ "lightpink",
+ "lightsalmon",
+ "lightseagreen",
+ "lightskyblue",
+ "lightslategray",
+ "lightslategrey",
+ "lightsteelblue",
+ "lightyellow",
+ "limegreen",
+ "linen",
+ "magenta",
+ "mediumaquamarine",
+ "mediumblue",
+ "mediumorchid",
+ "mediumpurple",
+ "mediumseagreen",
+ "mediumslateblue",
+ "mediumspringgreen",
+ "mediumturquoise",
+ "mediumvioletred",
+ "midnightblue",
+ "mintcream",
+ "mistyrose",
+ "moccasin",
+ "navajowhite",
+ "oldlace",
+ "olivedrab",
+ "orangered",
+ "orchid",
+ "palegoldenrod",
+ "palegreen",
+ "paleturquoise",
+ "palevioletred",
+ "papayawhip",
+ "peachpuff",
+ "peru",
+ "pink",
+ "plum",
+ "powderblue",
+ "rosybrown",
+ "royalblue",
+ "saddlebrown",
+ "salmon",
+ "sandybrown",
+ "seagreen",
+ "seashell",
+ "sienna",
+ "skyblue",
+ "slateblue",
+ "slategray",
+ "slategrey",
+ "snow",
+ "springgreen",
+ "steelblue",
+ "tan",
+ "thistle",
+ "tomato",
+ "turquoise",
+ "violet",
+ "wheat",
+ "whitesmoke",
+ "yellowgreen",
+ "rebeccapurple"
+]
diff --git a/apioforum/db.py b/apioforum/db.py
index 899c6b4..5c3d2eb 100644
--- a/apioforum/db.py
+++ b/apioforum/db.py
@@ -85,13 +85,46 @@ ALTER TABLE users ADD COLUMN bio TEXT;
ALTER TABLE users ADD COLUMN joined TIMESTAMP;
""",
"""
+CREATE TABLE polls (
+ id INTEGER PRIMARY KEY,
+ title TEXT NOT NULL
+);
+ALTER TABLE threads ADD COLUMN poll INTEGER REFERENCES polls(id);
+
+CREATE TABLE poll_options (
+ poll INTEGER NOT NULL REFERENCES polls(id),
+ text TEXT NOT NULL,
+ option_idx INTEGER NOT NULL,
+ PRIMARY KEY ( poll, option_idx )
+);
+
+CREATE TABLE votes (
+ id INTEGER PRIMARY KEY,
+ user TEXT NOT NULL REFERENCES users(username),
+ poll INTEGER NOT NULL,
+ option_idx INTEGER,
+ time TIMESTAMP NOT NULL,
+ current INTEGER NOT NULL,
+ is_retraction INTEGER,
+ CHECK (is_retraction OR (option_idx NOT NULL)),
+ FOREIGN KEY ( poll, option_idx ) REFERENCES poll_options(poll, option_idx)
+);
+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,'the default root forum');
+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;
@@ -101,7 +134,8 @@ CREATE TABLE threads_new (
creator TEXT NOT NULL,
created TIMESTAMP NOT NULL,
updated TIMESTAMP NOT NULL,
- forum NOT NULL REFERENCES forums(id)
+ 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;
@@ -118,6 +152,26 @@ CREATE VIEW number_of_posts AS
SELECT thread, count(*) AS num_replies FROM posts GROUP BY thread;
""",
"""
+CREATE VIEW total_vote_counts AS
+ SELECT poll, count(*) AS total_votes FROM votes WHERE current AND NOT is_retraction GROUP BY poll;
+""",
+"""
+PRAGMA foreign_keys = off;
+BEGIN TRANSACTION;
+CREATE TABLE tags_new (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL,
+ text_colour TEXT NOT NULL,
+ bg_colour TEXT NOT NULL,
+ forum INTEGER NOT NULL REFERENCES forums(id)
+);
+INSERT INTO tags_new (id,name,text_colour,bg_colour,forum)
+ SELECT id,name,text_colour,bg_colour,1 FROM tags;
+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),
@@ -149,7 +203,7 @@ ALTER TABLE posts ADD COLUMN deleted NOT NULL DEFAULT 0;
""",
"""
ALTER TABLE forums ADD COLUMN unlisted NOT NULL DEFAULT 0;
-"""
+""",
]
def init_db():
diff --git a/apioforum/forum.py b/apioforum/forum.py
index 410bee5..cae4eab 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -19,6 +19,19 @@ bp = Blueprint("forum", __name__, url_prefix="/")
def not_actual_index():
return redirect("/1")
+def get_avail_tags(forum_id):
+ db = get_db()
+ tags = 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 tags
+ WHERE tags.forum in (SELECT id FROM fs)
+ ORDER BY id;
+ """,(forum_id,)).fetchall()
+ return tags
+
def forum_path(forum_id):
db = get_db()
ancestors = db.execute("""
@@ -73,7 +86,7 @@ def view_forum(forum):
threads = db.execute(
"""SELECT
threads.id, threads.title, threads.creator, threads.created,
- threads.updated, number_of_posts.num_replies,
+ threads.updated, threads.poll, 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,
@@ -86,6 +99,10 @@ def view_forum(forum):
ORDER BY threads.updated DESC;
""",(forum['id'],)).fetchall()
thread_tags = {}
+ thread_polls = {}
+
+ avail_tags = get_avail_tags(forum['id'])
+
#todo: somehow optimise this
for thread in threads:
thread_tags[thread['id']] = db.execute(
@@ -95,6 +112,29 @@ def view_forum(forum):
ORDER BY tags.id;
""",(thread['id'],)).fetchall()
+ if thread['poll'] is not None:
+ # todo: make this not be duplicated from thread.py
+ 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
+ poll['total_votes']=poll['total_votes'] or 0
+ thread_polls[thread['id']]=poll
+
+
subforums_rows = db.execute("""
SELECT max(threads.updated) as updated, forums.* FROM forums
LEFT OUTER JOIN threads ON threads.forum=forums.id
@@ -121,7 +161,9 @@ def view_forum(forum):
subforums=subforums,
threads=threads,
thread_tags=thread_tags,
- bureaucrats=bureaucrats
+ bureaucrats=bureaucrats,
+ thread_polls=thread_polls,
+ avail_tags=avail_tags,
)
@forum_route("create_thread",methods=("GET","POST"))
@@ -351,7 +393,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/mdrender.py b/apioforum/mdrender.py
index 8c59c42..52a6e6c 100644
--- a/apioforum/mdrender.py
+++ b/apioforum/mdrender.py
@@ -1,4 +1,5 @@
import bleach
+from .csscolors import csscolors
allowed_tags = [
'p',
@@ -10,14 +11,28 @@ allowed_tags = [
'h6',
'pre',
'del',
+ 'ins',
'mark',
'img',
- 'marquee'
+ 'marquee',
+ 'pulsate',
+ 'sup','sub',
+ 'table','thead','tbody','tr','th','td',
+ 'details','summary',
+ 'hr'
+
+
+
]
+allowed_tags += csscolors
+allowed_tags += ("mark" + c for c in csscolors)
+
allowed_attributes = bleach.sanitizer.ALLOWED_ATTRIBUTES.copy()
allowed_attributes.update(
- img='src',
+ img=['src','alt','title'],
+ ol=['start'],
+ details=['open'],
)
allowed_tags.extend(bleach.sanitizer.ALLOWED_TAGS)
@@ -25,7 +40,13 @@ allowed_tags.extend(bleach.sanitizer.ALLOWED_TAGS)
cleaner = bleach.sanitizer.Cleaner(tags=allowed_tags,attributes=allowed_attributes)
import markdown
-md = markdown.Markdown(extensions=['pymdownx.tilde','fenced_code'])
+md = markdown.Markdown(extensions=[
+ 'pymdownx.tilde',
+ 'pymdownx.caret',
+ 'fenced_code',
+ 'tables',
+ 'pymdownx.details',
+])
def render(text):
text = md.reset().convert(text)
diff --git a/apioforum/static/md-colors.css b/apioforum/static/md-colors.css
new file mode 100644
index 0000000..e29da0c
--- /dev/null
+++ b/apioforum/static/md-colors.css
@@ -0,0 +1,306 @@
+
+pulsate { animation: 2s infinite alternate pulsate; }
+
+@keyframes pulsate {
+ from { letter-spacing: normal; }
+ to { letter-spacing: 1px; }
+}
+
+black { color: black; }
+silver { color: silver; }
+gray { color: gray; }
+white { color: white; }
+maroon { color: maroon; }
+red { color: red; }
+purple { color: purple; }
+fuchsia { color: fuchsia; }
+green { color: green; }
+lime { color: lime; }
+olive { color: olive; }
+yellow { color: yellow; }
+navy { color: navy; }
+blue { color: blue; }
+teal { color: teal; }
+aqua { color: aqua; }
+orange { color: orange; }
+aliceblue { color: aliceblue; }
+antiquewhite { color: antiquewhite; }
+aquamarine { color: aquamarine; }
+azure { color: azure; }
+beige { color: beige; }
+bisque { color: bisque; }
+blanchedalmond { color: blanchedalmond; }
+blueviolet { color: blueviolet; }
+brown { color: brown; }
+burlywood { color: burlywood; }
+cadetblue { color: cadetblue; }
+chartreuse { color: chartreuse; }
+chocolate { color: chocolate; }
+coral { color: coral; }
+cornflowerblue { color: cornflowerblue; }
+cornsilk { color: cornsilk; }
+crimson { color: crimson; }
+cyan { color: cyan; }
+darkblue { color: darkblue; }
+darkcyan { color: darkcyan; }
+darkgoldenrod { color: darkgoldenrod; }
+darkgray { color: darkgray; }
+darkgreen { color: darkgreen; }
+darkgrey { color: darkgrey; }
+darkkhaki { color: darkkhaki; }
+darkmagenta { color: darkmagenta; }
+darkolivegreen { color: darkolivegreen; }
+darkorange { color: darkorange; }
+darkorchid { color: darkorchid; }
+darkred { color: darkred; }
+darksalmon { color: darksalmon; }
+darkseagreen { color: darkseagreen; }
+darkslateblue { color: darkslateblue; }
+darkslategray { color: darkslategray; }
+darkslategrey { color: darkslategrey; }
+darkturquoise { color: darkturquoise; }
+darkviolet { color: darkviolet; }
+deeppink { color: deeppink; }
+deepskyblue { color: deepskyblue; }
+dimgray { color: dimgray; }
+dimgrey { color: dimgrey; }
+dodgerblue { color: dodgerblue; }
+firebrick { color: firebrick; }
+floralwhite { color: floralwhite; }
+forestgreen { color: forestgreen; }
+gainsboro { color: gainsboro; }
+ghostwhite { color: ghostwhite; }
+gold { color: gold; }
+goldenrod { color: goldenrod; }
+greenyellow { color: greenyellow; }
+grey { color: grey; }
+honeydew { color: honeydew; }
+hotpink { color: hotpink; }
+indianred { color: indianred; }
+indigo { color: indigo; }
+ivory { color: ivory; }
+khaki { color: khaki; }
+lavender { color: lavender; }
+lavenderblush { color: lavenderblush; }
+lawngreen { color: lawngreen; }
+lemonchiffon { color: lemonchiffon; }
+lightblue { color: lightblue; }
+lightcoral { color: lightcoral; }
+lightcyan { color: lightcyan; }
+lightgoldenrodyellow { color: lightgoldenrodyellow; }
+lightgray { color: lightgray; }
+lightgreen { color: lightgreen; }
+lightgrey { color: lightgrey; }
+lightpink { color: lightpink; }
+lightsalmon { color: lightsalmon; }
+lightseagreen { color: lightseagreen; }
+lightskyblue { color: lightskyblue; }
+lightslategray { color: lightslategray; }
+lightslategrey { color: lightslategrey; }
+lightsteelblue { color: lightsteelblue; }
+lightyellow { color: lightyellow; }
+limegreen { color: limegreen; }
+linen { color: linen; }
+magenta { color: magenta; }
+mediumaquamarine { color: mediumaquamarine; }
+mediumblue { color: mediumblue; }
+mediumorchid { color: mediumorchid; }
+mediumpurple { color: mediumpurple; }
+mediumseagreen { color: mediumseagreen; }
+mediumslateblue { color: mediumslateblue; }
+mediumspringgreen { color: mediumspringgreen; }
+mediumturquoise { color: mediumturquoise; }
+mediumvioletred { color: mediumvioletred; }
+midnightblue { color: midnightblue; }
+mintcream { color: mintcream; }
+mistyrose { color: mistyrose; }
+moccasin { color: moccasin; }
+navajowhite { color: navajowhite; }
+oldlace { color: oldlace; }
+olivedrab { color: olivedrab; }
+orangered { color: orangered; }
+orchid { color: orchid; }
+palegoldenrod { color: palegoldenrod; }
+palegreen { color: palegreen; }
+paleturquoise { color: paleturquoise; }
+palevioletred { color: palevioletred; }
+papayawhip { color: papayawhip; }
+peachpuff { color: peachpuff; }
+peru { color: peru; }
+pink { color: pink; }
+plum { color: plum; }
+powderblue { color: powderblue; }
+rosybrown { color: rosybrown; }
+royalblue { color: royalblue; }
+saddlebrown { color: saddlebrown; }
+salmon { color: salmon; }
+sandybrown { color: sandybrown; }
+seagreen { color: seagreen; }
+seashell { color: seashell; }
+sienna { color: sienna; }
+skyblue { color: skyblue; }
+slateblue { color: slateblue; }
+slategray { color: slategray; }
+slategrey { color: slategrey; }
+snow { color: snow; }
+springgreen { color: springgreen; }
+steelblue { color: steelblue; }
+tan { color: tan; }
+thistle { color: thistle; }
+tomato { color: tomato; }
+turquoise { color: turquoise; }
+violet { color: violet; }
+wheat { color: wheat; }
+whitesmoke { color: whitesmoke; }
+yellowgreen { color: yellowgreen; }
+rebeccapurple { color: rebeccapurple; }
+
+
+markblack { background-color: black; }
+marksilver { background-color: silver; }
+markgray { background-color: gray; }
+markwhite { background-color: white; }
+markmaroon { background-color: maroon; }
+markred { background-color: red; }
+markpurple { background-color: purple; }
+markfuchsia { background-color: fuchsia; }
+markgreen { background-color: green; }
+marklime { background-color: lime; }
+markolive { background-color: olive; }
+markyellow { background-color: yellow; }
+marknavy { background-color: navy; }
+markblue { background-color: blue; }
+markteal { background-color: teal; }
+markaqua { background-color: aqua; }
+markorange { background-color: orange; }
+markaliceblue { background-color: aliceblue; }
+markantiquewhite { background-color: antiquewhite; }
+markaquamarine { background-color: aquamarine; }
+markazure { background-color: azure; }
+markbeige { background-color: beige; }
+markbisque { background-color: bisque; }
+markblanchedalmond { background-color: blanchedalmond; }
+markblueviolet { background-color: blueviolet; }
+markbrown { background-color: brown; }
+markburlywood { background-color: burlywood; }
+markcadetblue { background-color: cadetblue; }
+markchartreuse { background-color: chartreuse; }
+markchocolate { background-color: chocolate; }
+markcoral { background-color: coral; }
+markcornflowerblue { background-color: cornflowerblue; }
+markcornsilk { background-color: cornsilk; }
+markcrimson { background-color: crimson; }
+markcyan { background-color: cyan; }
+markdarkblue { background-color: darkblue; }
+markdarkcyan { background-color: darkcyan; }
+markdarkgoldenrod { background-color: darkgoldenrod; }
+markdarkgray { background-color: darkgray; }
+markdarkgreen { background-color: darkgreen; }
+markdarkgrey { background-color: darkgrey; }
+markdarkkhaki { background-color: darkkhaki; }
+markdarkmagenta { background-color: darkmagenta; }
+markdarkolivegreen { background-color: darkolivegreen; }
+markdarkorange { background-color: darkorange; }
+markdarkorchid { background-color: darkorchid; }
+markdarkred { background-color: darkred; }
+markdarksalmon { background-color: darksalmon; }
+markdarkseagreen { background-color: darkseagreen; }
+markdarkslateblue { background-color: darkslateblue; }
+markdarkslategray { background-color: darkslategray; }
+markdarkslategrey { background-color: darkslategrey; }
+markdarkturquoise { background-color: darkturquoise; }
+markdarkviolet { background-color: darkviolet; }
+markdeeppink { background-color: deeppink; }
+markdeepskyblue { background-color: deepskyblue; }
+markdimgray { background-color: dimgray; }
+markdimgrey { background-color: dimgrey; }
+markdodgerblue { background-color: dodgerblue; }
+markfirebrick { background-color: firebrick; }
+markfloralwhite { background-color: floralwhite; }
+markforestgreen { background-color: forestgreen; }
+markgainsboro { background-color: gainsboro; }
+markghostwhite { background-color: ghostwhite; }
+markgold { background-color: gold; }
+markgoldenrod { background-color: goldenrod; }
+markgreenyellow { background-color: greenyellow; }
+markgrey { background-color: grey; }
+markhoneydew { background-color: honeydew; }
+markhotpink { background-color: hotpink; }
+markindianred { background-color: indianred; }
+markindigo { background-color: indigo; }
+markivory { background-color: ivory; }
+markkhaki { background-color: khaki; }
+marklavender { background-color: lavender; }
+marklavenderblush { background-color: lavenderblush; }
+marklawngreen { background-color: lawngreen; }
+marklemonchiffon { background-color: lemonchiffon; }
+marklightblue { background-color: lightblue; }
+marklightcoral { background-color: lightcoral; }
+marklightcyan { background-color: lightcyan; }
+marklightgoldenrodyellow { background-color: lightgoldenrodyellow; }
+marklightgray { background-color: lightgray; }
+marklightgreen { background-color: lightgreen; }
+marklightgrey { background-color: lightgrey; }
+marklightpink { background-color: lightpink; }
+marklightsalmon { background-color: lightsalmon; }
+marklightseagreen { background-color: lightseagreen; }
+marklightskyblue { background-color: lightskyblue; }
+marklightslategray { background-color: lightslategray; }
+marklightslategrey { background-color: lightslategrey; }
+marklightsteelblue { background-color: lightsteelblue; }
+marklightyellow { background-color: lightyellow; }
+marklimegreen { background-color: limegreen; }
+marklinen { background-color: linen; }
+markmagenta { background-color: magenta; }
+markmediumaquamarine { background-color: mediumaquamarine; }
+markmediumblue { background-color: mediumblue; }
+markmediumorchid { background-color: mediumorchid; }
+markmediumpurple { background-color: mediumpurple; }
+markmediumseagreen { background-color: mediumseagreen; }
+markmediumslateblue { background-color: mediumslateblue; }
+markmediumspringgreen { background-color: mediumspringgreen; }
+markmediumturquoise { background-color: mediumturquoise; }
+markmediumvioletred { background-color: mediumvioletred; }
+markmidnightblue { background-color: midnightblue; }
+markmintcream { background-color: mintcream; }
+markmistyrose { background-color: mistyrose; }
+markmoccasin { background-color: moccasin; }
+marknavajowhite { background-color: navajowhite; }
+markoldlace { background-color: oldlace; }
+markolivedrab { background-color: olivedrab; }
+markorangered { background-color: orangered; }
+markorchid { background-color: orchid; }
+markpalegoldenrod { background-color: palegoldenrod; }
+markpalegreen { background-color: palegreen; }
+markpaleturquoise { background-color: paleturquoise; }
+markpalevioletred { background-color: palevioletred; }
+markpapayawhip { background-color: papayawhip; }
+markpeachpuff { background-color: peachpuff; }
+markperu { background-color: peru; }
+markpink { background-color: pink; }
+markplum { background-color: plum; }
+markpowderblue { background-color: powderblue; }
+markrosybrown { background-color: rosybrown; }
+markroyalblue { background-color: royalblue; }
+marksaddlebrown { background-color: saddlebrown; }
+marksalmon { background-color: salmon; }
+marksandybrown { background-color: sandybrown; }
+markseagreen { background-color: seagreen; }
+markseashell { background-color: seashell; }
+marksienna { background-color: sienna; }
+markskyblue { background-color: skyblue; }
+markslateblue { background-color: slateblue; }
+markslategray { background-color: slategray; }
+markslategrey { background-color: slategrey; }
+marksnow { background-color: snow; }
+markspringgreen { background-color: springgreen; }
+marksteelblue { background-color: steelblue; }
+marktan { background-color: tan; }
+markthistle { background-color: thistle; }
+marktomato { background-color: tomato; }
+markturquoise { background-color: turquoise; }
+markviolet { background-color: violet; }
+markwheat { background-color: wheat; }
+markwhitesmoke { background-color: whitesmoke; }
+markyellowgreen { background-color: yellowgreen; }
+markrebeccapurple { background-color: rebeccapurple; }
diff --git a/apioforum/static/style.css b/apioforum/static/style.css
index 2ed2e7a..62d643c 100644
--- a/apioforum/static/style.css
+++ b/apioforum/static/style.css
@@ -16,8 +16,8 @@ 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,.username {
+.post-heading,.post-footer { font-size: smaller; }
+.post-heading,.post-footer,.username {
color: hsl(0,0%,25%);
}
.username {
@@ -32,6 +32,8 @@ body { font-family: sans-serif; word-wrap: break-word; }
.post-heading-a { margin-left: 0.2em }
.post-heading-b { float: right; margin-right: 0.5em }
+.post-footer { margin-left: 0.2em; font-style: italic; }
+
.post-anchor-link { color: hsl(0,0%,25%); }
.deleted-post {
@@ -51,6 +53,10 @@ body { font-family: sans-serif; word-wrap: break-word; }
color: hsl(0,0%,80%);
}
+.deleted-post > .post-footer {
+ color: hsl(0,0%,80%);
+}
+
.thread-top-bar, .user-top-bar {
margin-bottom: 4px;
}
@@ -95,7 +101,6 @@ dt { font-weight: bold }
img { max-width: 100% }
-
nav#navbar {
float: right;
padding: 5px;
@@ -162,6 +167,11 @@ nav#navbar .links { display: flex; }
.thread-preview-post { font-style: italic; }
.thread-preview-ts { font-weight: bold; }
+.thread-vote-summary {
+ margin-top: 4px;
+ margin-bottom: -8px;
+}
+
/* wide screens */
@media all and (min-width: 600px) {
.listing-title { font-size: larger; }
@@ -196,6 +206,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;
@@ -213,7 +233,7 @@ fieldset { margin-bottom: 15px; }
.warning { color: red; font-weight: bold }
-.search-form {
+.inline-form {
display: inline-block;
}
@@ -223,6 +243,15 @@ fieldset { margin-bottom: 15px; }
border: 1px solid black;
}
+.md table {
+ border: 1px solid grey;
+ border-collapse: collapse;
+}
+.md table td,.md table th {
+ border: 1px solid grey;
+ padding: 4px;
+}
+
.role-input, .name-input { width: 12ch; }
.thing-id { color: darkgray; font-size: smaller; font-weight: normal; }
diff --git a/apioforum/templates/base.html b/apioforum/templates/base.html
index f462df2..ca1dd87 100644
--- a/apioforum/templates/base.html
+++ b/apioforum/templates/base.html
@@ -6,12 +6,13 @@
<title>{%block title %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css">
+ <link rel="stylesheet" href="/static/md-colors.css">
<link rel="icon" href="//gh0.pw/favicon.ico">
</head>
<body>
<nav id="navbar">
<p style="font-family: monospace;"><b>apio</b><i>forum</i>&trade;</p>
- <form class="search-form" action="/search">
+ <form class="inline-form" action="/search">
<input type="search" placeholder="query" name="q">
<input type="submit" value="search">
</form>
diff --git a/apioforum/templates/common.html b/apioforum/templates/common.html
index 7144667..f6b6f29 100644
--- a/apioforum/templates/common.html
+++ b/apioforum/templates/common.html
@@ -6,7 +6,7 @@
{{url_for('thread.view_thread', thread_id=post.thread)}}#post_{{post.id}}
{%- endmacro %}
-{% macro disp_post(post, buttons=False, forum=None) %}
+{% 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">
@@ -45,17 +45,25 @@
<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>
</span>
</div>
- <div class="post-content">
+ <div class="post-content md">
{% if not post.deleted %}
{{ post.content|md|safe }}
{% else %}
this post never existed.
{% endif %}
</div>
+ {% if footer %}
+ <div class="post-footer">
+ {{ footer }}
+ </div>
+ {% endif %}
</div>
{% endmacro %}
@@ -97,3 +105,40 @@
<li>{{ thread.title }}</li>
{% endcall -%}
{% endmacro %}
+
+{% macro vote_meter(poll) %}
+ {% set total_votes = poll.total_votes %}
+ {% set n = namespace() %}
+ {% set n.runningtotal = 0 %}
+ <svg width="100%" height="15px" xmlns="http://www.w3.org/2000/svg">
+ {% if total_votes == 0 %}
+ <text text-anchor="middle" dominant-baseline="middle" x="11%" y="55%" fill="black" style="font-size:15px">no votes</text>
+ {% else %}
+ {% for opt in poll.options %}
+ {% set opt_count = opt.num or 0 %}
+ {% set colour = (loop.index|string + opt.text)|gen_colour %}
+ {% if opt_count != 0 %}
+ {% set percentage = 100*(opt_count/total_votes) %}
+ {# todo: do this in css somehow #}
+ {% if opt.text|length > 10 %}
+ {% set opt_text = opt.text[:7] + "..." %}
+ {% else %}
+ {% set opt_text = opt.text %}
+ {% endif %}
+ <rect y="0" height="100%" x="{{n.runningtotal}}%" width="{{percentage}}%" stroke="black" fill="{{colour}}" />
+ <text text-anchor="middle" dominant-baseline="middle" y="55%" fill="black" style="font-size:15px" x="{{n.runningtotal+(percentage/2)}}%">
+ {{opt_text}}: {{opt_count}}
+ </text>
+ {% set n.runningtotal = n.runningtotal + percentage %}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ <desc>
+ poll: {{poll.title}}
+ {% for opt in poll.options %}
+ option "{{opt.text}}": {{opt.num or 0}} votes
+ {% endfor %}
+ total votes: {{total_votes}}
+ </desc>
+ </svg>
+{% endmacro %}
diff --git a/apioforum/templates/config_thread.html b/apioforum/templates/config_thread.html
index b26a73d..7403614 100644
--- a/apioforum/templates/config_thread.html
+++ b/apioforum/templates/config_thread.html
@@ -2,6 +2,7 @@
{% from 'common.html' import tag %}
{% block header %}<h1>{% block title %}configure thread '{{thread.title}}'{% endblock %}</h1>{% endblock %}
{% block content %}
+<h2>thread options</h2>
<form method="post">
<fieldset>
<legend>title</legend>
@@ -27,4 +28,28 @@
<input type="submit" value="confirm">
<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" %}
+<h2>create poll</h2>
+<form method="post" action="{{url_for('thread.create_poll',thread_id=thread.id)}}">
+ <fieldset>
+ <legend>create poll</legend>
+ <label for="polltitle">question title</label>
+ <input type="title" id="polltitle" name="polltitle">
+ <br>
+ <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>
+ <input type="submit" value="create">
+</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>
+<form action="{{url_for('thread.delete_poll',thread_id=thread.id)}}" method="post">
+ <input type="submit" value="confirm: delete poll">
+</form>
+
+{% endif %}
+
{% endblock %}
diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html
index 863f91c..ff1af9b 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, ab %}
+{% from 'common.html' import ts, tag, disp_user, post_url, forum_breadcrumb, ab, vote_meter %}
{% block header %}
<h1>{% block title %}{{forum.name}}{% endblock %} <span class="thing-id">#{{forum.id}}</span></h1>
{% if forum.id != 1 %}
@@ -8,15 +8,28 @@
{%endblock%}
{%block content%}
-{{forum.description|md|safe}}
-{% if bureaucrats|length > 0 %}
- <p>
- bureaucrats in this forum:
- {% for b in bureaucrats %}
- {{disp_user(b)}}
- {% 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 %}
+
+ <p>available tags:
+ {% for the_tag in avail_tags %}
+ {{tag(the_tag)}}
+ {% else %}
+ <em>(none available)</em>
+ {% endfor %}
</p>
-{% endif %}
+</div>
+
<p>
{% if is_bureaucrat(forum.id, g.user) %}
{{ab("forum settings",url_for('forum.edit_forum',forum_id=forum.id))}}
@@ -30,6 +43,7 @@
{{ab("approve users",url_for('forum.view_user_role',forum_id=forum.id))}}
{% endif %}
</p>
+
{% if subforums %}
<h2>subforæ</h2>
<div class="forum-list">
@@ -55,10 +69,11 @@
{% endif %}
<h2>threads</h2>
+<p>
{% if g.user %}
-<p><a class="actionbutton" href="{{url_for('forum.create_thread',forum_id=forum.id)}}">create new thread</a></p>
+<a class="actionbutton" href="{{url_for('forum.create_thread',forum_id=forum.id)}}">create new thread</a>
{% else %}
-<p>please log in to create a new thread</p>
+please log in to create a new thread
{% endif %}
{% if has_permission(forum.id, g.user, "p_view_threads") %}
@@ -103,11 +118,15 @@
</a>
</div>
{% endif %}
+ {% if thread_polls[thread.id] %}
+ <div class="thread-vote-summary">
+ {{ vote_meter(thread_polls[thread.id]) }}
+ </div>
+ {% endif %}
</div>
{%endfor%}
</div>
{% else %}
<p>you do not have permission to view threads in this forum</p>
{% endif %}
-
{%endblock%}
diff --git a/apioforum/templates/view_post.html b/apioforum/templates/view_post.html
new file mode 100644
index 0000000..993c005
--- /dev/null
+++ b/apioforum/templates/view_post.html
@@ -0,0 +1,12 @@
+{% from 'common.html' import disp_post %}
+{% extends 'base.html' %}
+{% block header %}
+<h1>{%block title%}viewing post{%endblock%}</h1>
+{% endblock %}
+
+{% block content %}
+{{disp_post(post,False)}}
+<p>post source:</p>
+<textarea readonly class="new-post-box" name="newcontent">{{post.content}}</textarea>
+<a href="{{url_for('thread.view_thread',thread_id=post.thread)}}">i am satisfied</a>
+{% endblock %}
diff --git a/apioforum/templates/view_thread.html b/apioforum/templates/view_thread.html
index da8df74..132fd44 100644
--- a/apioforum/templates/view_thread.html
+++ b/apioforum/templates/view_thread.html
@@ -1,4 +1,4 @@
-{% from 'common.html' import disp_post,tag,thread_breadcrumb %}
+{% from 'common.html' import disp_post,tag,thread_breadcrumb,vote_meter %}
{% extends 'base.html' %}
{% block header %}
<h1>{%block title %}{{thread.title}}{% endblock %} <span class="thing-id">#{{thread.id}}</span></h1>
@@ -6,6 +6,15 @@
{% endblock %}
{%block content%}
+{% if poll %}
+<p>{{poll.title}}</p>
+<ol>
+ {%for opt in poll.options%}
+ <li value="{{opt.option_idx}}"><i>{{opt.text}}</i>: {{opt.num or 0}} votes</li>
+ {%endfor%}
+</ol>
+{{ vote_meter(poll) }}
+{% endif %}
<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") %}
@@ -25,14 +34,70 @@
<div class="posts">
{% for post in posts %}
- {{ disp_post(post, buttons=True, forum=thread.forum) }}
+ {% 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 %}
+ {% 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 %}
+ this post presented a vote that was later changed
+ {% endif %}
+ {% endif %}
+ {% endif %}
+
+ {% endset %}
+
+ {{ disp_post(post, forum=thread.forum, buttons=True, footer=footer) }}
+
+ {% else %}
+ {{ 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 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
+ on this poll, your previous vote will be changed</p>
+
+ <input type="radio" id="dontvote" name="poll" value="dontvote" checked>
+ <label for="dontvote">do not submit any vote at the moment</label>
+
+ {% if has_voted %}
+ <br>
+ <input type="radio" id="retractvote" name="poll" value="retractvote">
+ <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}}">#{{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/thread.py b/apioforum/thread.py
index 1291adf..0b0804e 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -1,11 +1,14 @@
# view posts in thread
+import itertools
+
from flask import (
Blueprint, render_template, abort, request, g, redirect,
- url_for, flash
+ 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,17 +23,158 @@ def view_thread(thread_id):
abort(404)
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()
+ 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()
- return render_template("view_thread.html",posts=posts,thread=thread,tags=tags)
+ 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,
+ )
+
+def register_vote(thread,pollval):
+ if pollval is None or pollval == 'dontvote':
+ return
+
+ is_retraction = pollval == 'retractvote'
+
+ if is_retraction:
+ option_idx = None
+ else:
+ option_idx = int(pollval)
+
+ db = get_db()
+ cur = db.cursor()
+ cur.execute("""
+ UPDATE votes
+ SET current = 0
+ WHERE poll = ? AND user = ?;
+ """,(thread['poll'],g.user))
+
+ cur.execute("""
+ INSERT INTO votes (user,poll,option_idx,time,current,is_retraction)
+ VALUES (?,?,?,current_timestamp,1,?);
+ """,(g.user,thread['poll'],option_idx,is_retraction))
+ vote_id = cur.lastrowid
+ return vote_id
+
+@bp.route("/<int:thread_id>/create_poll",methods=["POST"])
+def create_poll(thread_id):
+ fail = redirect(url_for('thread.config_thread',thread_id=thread_id))
+ success = redirect(url_for('thread.view_thread',thread_id=thread_id))
+ err = None
+ db = get_db()
+ thread = db.execute('select * from threads where id = ?',(thread_id,)).fetchone()
+
+ polltitle = request.form.get('polltitle','').strip()
+ polloptions = [q.strip() for q in request.form.get('polloptions','').split("\n") if len(q.strip()) > 0]
+
+ if thread is None:
+ 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'] 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"
+ elif not len(polltitle) > 0:
+ 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)
+ return fail
+ else:
+ cur = db.cursor()
+ cur.execute("INSERT INTO polls (title) VALUES (?)",(polltitle,))
+ pollid = cur.lastrowid
+ cur.execute("UPDATE threads SET poll = ? WHERE threads.id = ?",(pollid,thread_id))
+ cur.executemany(
+ "INSERT INTO poll_options (poll,option_idx,text) VALUES (?,?,?)",
+ zip(itertools.repeat(pollid),itertools.count(1),polloptions)
+ )
+ db.commit()
+ flash("poll created successfully")
+ return success
+@bp.route("/<int:thread_id>/delete_poll",methods=["POST"])
+def delete_poll(thread_id):
+ fail = redirect(url_for('thread.config_thread',thread_id=thread_id))
+ success = redirect(url_for('thread.view_thread',thread_id=thread_id))
+ err = None
+ db = get_db()
+ thread = db.execute('select * from threads where id = ?',(thread_id,)).fetchone()
+
+ if thread is None:
+ 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'] 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"
+
+ if err is not None:
+ flash(err)
+ 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,))
+ db.execute("UPDATE THREADS set poll = NULL WHERE id = ?",(thread_id,))
+ db.execute("DELETE FROM polls WHERE id = ?",(pollid,))
+ db.commit()
+ flash("poll deleted successfully")
+ return success
+
@bp.route("/<int:thread_id>/create_post", methods=("POST",))
def create_post(thread_id):
if g.user is None:
@@ -46,12 +190,24 @@ def create_post(thread_id):
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:
+ 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) VALUES (?,?,?,current_timestamp);",
- (thread_id,g.user,content)
- )
+ 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 = ?;",
@@ -113,8 +269,7 @@ def edit_post(post_id):
post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone()
if post is None:
flash("that post doesn't exist")
- # todo: index route
- return redirect("/")
+ return redirect(url_for('index'))
if post['author'] != g.user:
flash("you can only edit posts that you created")
@@ -136,13 +291,26 @@ def edit_post(post_id):
else:
flash(err)
return render_template("edit_post.html",post=post)
+
+@bp.route("/view_post/<int:post_id>")
+def view_post(post_id):
+ db = get_db()
+ post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone()
+ if post is None:
+ flash("that post doesn't exist")
+ return redirect(url_for('index'))
+
+ # when we have permissions, insert permissions check here
+ return render_template("view_post.html",post=post)
+
+
@bp.route("/<int:thread_id>/config",methods=["GET","POST"])
def config_thread(thread_id):
db = get_db()
thread = db.execute("select * from threads where id = ?",(thread_id,)).fetchone()
thread_tags = [r['tag'] for r in db.execute("select tag from thread_tags where thread = ?",(thread_id,)).fetchall()]
- avail_tags = db.execute("select * from tags order by id").fetchall()
+ avail_tags = get_avail_tags(thread['forum'])
err = None
if g.user is None:
err = "you need to be logged in to do that"
@@ -164,8 +332,8 @@ def config_thread(thread_id):
flash("title updated successfully")
db.commit()
changed = False
- wanted_tags = []
- for tagid in range(1,len(avail_tags)+1):
+ for avail_tag in avail_tags:
+ tagid = avail_tag['id']
current = tagid in thread_tags
wanted = f'tag_{tagid}' in request.form
if wanted and not current:
diff --git a/apioforum/util.py b/apioforum/util.py
new file mode 100644
index 0000000..64bdf20
--- /dev/null
+++ b/apioforum/util.py
@@ -0,0 +1,16 @@
+# various utility things
+
+import hsluv
+import hashlib
+
+# same algorithm as xep-0392
+def gen_colour(s):
+ b=s.encode("utf-8")
+ h=hashlib.sha1(b)
+ two_bytes=h.digest()[:2]
+ val = int.from_bytes(two_bytes, 'little')
+ angle = 360*(val/65536)
+ col = hsluv.hsluv_to_hex([angle, 80, 70])
+ return col
+
+
diff --git a/setup.py b/setup.py
index 089f3a4..15992ed 100644
--- a/setup.py
+++ b/setup.py
@@ -10,5 +10,6 @@ setup(
'markdown',
'bleach',
'pymdown-extensions',
+ 'hsluv',
],
)