aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--apioforum/csscolors.py150
-rw-r--r--apioforum/db.py39
-rw-r--r--apioforum/forum.py2
-rw-r--r--apioforum/mdrender.py27
-rw-r--r--apioforum/static/md-colors.css306
-rw-r--r--apioforum/static/style.css15
-rw-r--r--apioforum/templates/base.html1
-rw-r--r--apioforum/templates/common.html24
-rw-r--r--apioforum/templates/config_thread.html25
-rw-r--r--apioforum/templates/view_forum.html2
-rw-r--r--apioforum/templates/view_post.html12
-rw-r--r--apioforum/templates/view_thread.html56
-rw-r--r--apioforum/thread.py184
13 files changed, 812 insertions, 31 deletions
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 7dd635e..97bd0e2 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;
@@ -117,7 +151,6 @@ CREATE VIEW most_recent_posts AS
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 7d6f0f0..1d26f3a 100644
--- a/apioforum/forum.py
+++ b/apioforum/forum.py
@@ -136,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/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 4403f18..62215c7 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,a.username {
+.post-heading,.post-footer { font-size: smaller; }
+.post-heading,.post-footer,a.username {
color: hsl(0,0%,25%);
}
a.username {
@@ -32,6 +32,8 @@ a.username {
.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%); }
.thread-top-bar, .user-top-bar {
@@ -191,6 +193,15 @@ blockquote {
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;
+}
+
.breadcrumbs {
list-style: none;
}
diff --git a/apioforum/templates/base.html b/apioforum/templates/base.html
index f462df2..9cfd7f3 100644
--- a/apioforum/templates/base.html
+++ b/apioforum/templates/base.html
@@ -6,6 +6,7 @@
<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>
diff --git a/apioforum/templates/common.html b/apioforum/templates/common.html
index b0bf713..44cfbce 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) %}
+{% macro disp_post(post, buttons=False, footer=None) %}
<div class="post" id="post_{{post.id}}">
<div class="post-heading">
<span class="post-heading-a">
@@ -17,18 +17,28 @@
{% endif %}
</span>
<span class="post-heading-b">
- {% if buttons and post.author == g.user %}
- <a class="actionbutton"
- href="{{url_for('thread.edit_post',post_id=post.id)}}">edit</a>
+ {% 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 %}
<a class="actionbutton"
- href="{{url_for('thread.delete_post',post_id=post.id)}}">delete</a>
+ 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">
+ <div class="post-content md">
{{ post.content|md|safe }}
</div>
+ {% if footer %}
+ <div class="post-footer">
+ {{ footer }}
+ </div>
+ {% endif %}
</div>
{% endmacro %}
diff --git a/apioforum/templates/config_thread.html b/apioforum/templates/config_thread.html
index b26a73d..2c9804e 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 %}
+<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">potential 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 96c51bb..d075d85 100644
--- a/apioforum/templates/view_forum.html
+++ b/apioforum/templates/view_forum.html
@@ -79,5 +79,5 @@
</div>
{%endfor%}
</div>
-
+</main>
{%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 dd41d87..d4a43ef 100644
--- a/apioforum/templates/view_thread.html
+++ b/apioforum/templates/view_thread.html
@@ -6,6 +6,14 @@
{% endblock %}
{%block content%}
+{% if poll %}
+<p>{{poll.title}}</p>
+<ul>
+ {%for opt in poll.options%}
+ <li>#{{opt.option_idx}} - {{opt.text}} - {{opt.num or 0}}</li>
+ {%endfor%}
+</ul>
+{% endif %}
<div class="thread-top-bar">
<span class="thread-top-bar-a">
{% if g.user == thread.creator %}
@@ -22,12 +30,58 @@
<div class="posts">
{% for post in posts %}
- {{ disp_post(post, True) }}
+ {% 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 %}
+ {{post.author}} retracted their vote
+ {% 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
+ {% endif %}
+ {% endif %}
+
+ {% endset %}
+
+ {{ disp_post(post, buttons=True, footer=footer) }}
+
+ {% else %}
+ {{ disp_post(post, buttons=True) }}
+ {% endif %}
{% endfor %}
</div>
{% if g.user %}
<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 %}
+ <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">retract my vote, and go back to having no vote on this poll</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>
+ {% endfor %}
+ </fieldset>
+ {% endif %}
<input type="submit" value="yes">
</form>
{% else %}
diff --git a/apioforum/thread.py b/apioforum/thread.py
index 4bb3c86..daf0b85 100644
--- a/apioforum/thread.py
+++ b/apioforum/thread.py
@@ -1,8 +1,10 @@
# 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
@@ -18,17 +20,151 @@ def view_thread(thread_id):
if thread is None:
abort(404)
else:
- 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 * FROM polls where 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']:
+ 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"
+
+ 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']:
+ 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:
@@ -43,11 +179,20 @@ def create_post(thread_id):
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))
+
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 = ?;",
@@ -55,7 +200,8 @@ def create_post(thread_id):
)
db.commit()
flash("post posted postfully")
- return redirect(post_jump(thread_id, post_id))
+ 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):
@@ -84,8 +230,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")
@@ -107,6 +252,19 @@ 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):