diff options
| -rw-r--r-- | apioforum/__init__.py | 6 | ||||
| -rw-r--r-- | apioforum/csscolors.py | 150 | ||||
| -rw-r--r-- | apioforum/db.py | 60 | ||||
| -rw-r--r-- | apioforum/forum.py | 48 | ||||
| -rw-r--r-- | apioforum/mdrender.py | 27 | ||||
| -rw-r--r-- | apioforum/static/md-colors.css | 306 | ||||
| -rw-r--r-- | apioforum/static/style.css | 37 | ||||
| -rw-r--r-- | apioforum/templates/base.html | 3 | ||||
| -rw-r--r-- | apioforum/templates/common.html | 49 | ||||
| -rw-r--r-- | apioforum/templates/config_thread.html | 25 | ||||
| -rw-r--r-- | apioforum/templates/view_forum.html | 43 | ||||
| -rw-r--r-- | apioforum/templates/view_post.html | 12 | ||||
| -rw-r--r-- | apioforum/templates/view_thread.html | 71 | ||||
| -rw-r--r-- | apioforum/thread.py | 198 | ||||
| -rw-r--r-- | apioforum/util.py | 16 | ||||
| -rw-r--r-- | setup.py | 1 | 
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>™</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 +     +     @@ -10,5 +10,6 @@ setup(          'markdown',          'bleach',          'pymdown-extensions', +        'hsluv',      ],  ) | 
