diff options
-rw-r--r-- | apioforum/csscolors.py | 150 | ||||
-rw-r--r-- | apioforum/db.py | 39 | ||||
-rw-r--r-- | apioforum/forum.py | 2 | ||||
-rw-r--r-- | apioforum/mdrender.py | 27 | ||||
-rw-r--r-- | apioforum/static/md-colors.css | 306 | ||||
-rw-r--r-- | apioforum/static/style.css | 15 | ||||
-rw-r--r-- | apioforum/templates/base.html | 1 | ||||
-rw-r--r-- | apioforum/templates/common.html | 24 | ||||
-rw-r--r-- | apioforum/templates/config_thread.html | 25 | ||||
-rw-r--r-- | apioforum/templates/view_forum.html | 2 | ||||
-rw-r--r-- | apioforum/templates/view_post.html | 12 | ||||
-rw-r--r-- | apioforum/templates/view_thread.html | 56 | ||||
-rw-r--r-- | apioforum/thread.py | 184 |
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): |