diff options
author | citrons <citrons@mondecitronne.com> | 2025-06-14 01:03:33 -0500 |
---|---|---|
committer | ubq323 <ubq323@ubq323.website> | 2025-06-14 10:17:34 +0100 |
commit | a62beb7a48044686e50a27a53d112a99f8607461 (patch) | |
tree | f05b91f15e299cbc8bd696f41664994aa8f45cd9 | |
parent | 53f8b3fd8adf27ad8a4df5036a7776ff8f6d17cc (diff) |
convert spaces to tabs
27 files changed, 1870 insertions, 1870 deletions
diff --git a/apioforum/__init__.py b/apioforum/__init__.py index a7d70c0..8a917c9 100644 --- a/apioforum/__init__.py +++ b/apioforum/__init__.py @@ -6,73 +6,73 @@ from .db import get_db import os def create_app(): - app = Flask(__name__, instance_relative_config=True) - app.config.from_mapping( - SECRET_KEY="dev", - DATABASE=os.path.join(app.instance_path, 'database.db'), - ) - app.config.from_pyfile("config.py",silent=True) - try: - os.makedirs(app.instance_path) - 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 - permissions.init_app(app) - - from . import auth - app.register_blueprint(auth.bp) - - from . import forum - app.register_blueprint(forum.bp) - - from . import thread - app.register_blueprint(thread.bp) - - from . import admin - app.register_blueprint(admin.bp) - - from . import user - app.register_blueprint(user.bp) - - 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 - if len(request.query_string) > 0 and not p.startswith("/auth"): - p += "?" + request.query_string.decode("utf-8") - return dict(path_for_next=p) - - app.jinja_env.globals.update(forum_path=forum.forum_path) - app.jinja_env.globals.update(post_jump=thread.post_jump) - from .roles import has_permission, is_bureaucrat, get_user_role - app.jinja_env.globals.update( - has_permission=has_permission, - is_bureaucrat=is_bureaucrat, - get_user_role=get_user_role) - - from .mdrender import render - @app.template_filter('md') - def md_render(s): - return render(s) - - @app.errorhandler(404) - def not_found(e): - return render_template('err/404.html'), 404 - @app.errorhandler(403) - def forbidden(e): - return render_template('err/403.html'), 403 - - app.add_url_rule("/",endpoint="index") - - return app + app = Flask(__name__, instance_relative_config=True) + app.config.from_mapping( + SECRET_KEY="dev", + DATABASE=os.path.join(app.instance_path, 'database.db'), + ) + app.config.from_pyfile("config.py",silent=True) + try: + os.makedirs(app.instance_path) + 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 + permissions.init_app(app) + + from . import auth + app.register_blueprint(auth.bp) + + from . import forum + app.register_blueprint(forum.bp) + + from . import thread + app.register_blueprint(thread.bp) + + from . import admin + app.register_blueprint(admin.bp) + + from . import user + app.register_blueprint(user.bp) + + 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 + if len(request.query_string) > 0 and not p.startswith("/auth"): + p += "?" + request.query_string.decode("utf-8") + return dict(path_for_next=p) + + app.jinja_env.globals.update(forum_path=forum.forum_path) + app.jinja_env.globals.update(post_jump=thread.post_jump) + from .roles import has_permission, is_bureaucrat, get_user_role + app.jinja_env.globals.update( + has_permission=has_permission, + is_bureaucrat=is_bureaucrat, + get_user_role=get_user_role) + + from .mdrender import render + @app.template_filter('md') + def md_render(s): + return render(s) + + @app.errorhandler(404) + def not_found(e): + return render_template('err/404.html'), 404 + @app.errorhandler(403) + def forbidden(e): + return render_template('err/403.html'), 403 + + app.add_url_rule("/",endpoint="index") + + return app diff --git a/apioforum/admin.py b/apioforum/admin.py index b11b735..f96b0c8 100644 --- a/apioforum/admin.py +++ b/apioforum/admin.py @@ -1,5 +1,5 @@ from flask import ( - Blueprint, render_template + Blueprint, render_template ) from .db import get_db from .permissions import admin_required @@ -9,6 +9,6 @@ bp = Blueprint("admin",__name__,url_prefix="/admin") @bp.route("/") @admin_required def admin_page(): - db = get_db() - admins = db.execute("select * from users where admin > 0;").fetchall() - return render_template("admin/admin_page.html",admins=admins) + db = get_db() + admins = db.execute("select * from users where admin > 0;").fetchall() + return render_template("admin/admin_page.html",admins=admins) diff --git a/apioforum/auth.py b/apioforum/auth.py index 8864fc0..93aafdd 100644 --- a/apioforum/auth.py +++ b/apioforum/auth.py @@ -1,6 +1,6 @@ from flask import ( - Blueprint, session, request, url_for, render_template, redirect, - flash, g + Blueprint, session, request, url_for, render_template, redirect, + flash, g ) from werkzeug.security import check_password_hash, generate_password_hash from .db import get_db @@ -9,121 +9,121 @@ import functools bp = Blueprint("auth", __name__, url_prefix="/auth") def get_next(): - return request.args.get('next',url_for('index')) + return request.args.get('next',url_for('index')) @bp.route("/login",methods=('GET','POST')) def login(): - if request.method == "POST": - username = request.form["username"] - password = request.form["password"] - db = get_db() - err = None - user = db.execute( - "SELECT password FROM users WHERE username = ?;",(username,) - ).fetchone() - if not username: - err = "username required" - elif not password: - err = "password required" - elif user is None or not check_password_hash(user['password'], password): - err = "invalid login" + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] + db = get_db() + err = None + user = db.execute( + "SELECT password FROM users WHERE username = ?;",(username,) + ).fetchone() + if not username: + err = "username required" + elif not password: + err = "password required" + elif user is None or not check_password_hash(user['password'], password): + err = "invalid login" - if err is None: - session.clear() - session['user'] = username - if 'keep_logged_in' in request.form: - session['keep_logged_in']=True - session.permanent = True - flash("logged in successfully") - return redirect(get_next()) + if err is None: + session.clear() + session['user'] = username + if 'keep_logged_in' in request.form: + session['keep_logged_in']=True + session.permanent = True + flash("logged in successfully") + return redirect(get_next()) - flash(err) - - return render_template("auth/login.html") + flash(err) + + return render_template("auth/login.html") @bp.route("/register", methods=("GET","POST")) def register(): - if request.method == "POST": - username = request.form["username"] - password = request.form["password"] - db = get_db() - err = None - if not username: - err = "Username required" - elif not password: - err = "Password required" - elif db.execute( - "SELECT 1 FROM users WHERE username = ?;", (username,) - ).fetchone() is not None: - err = f"User {username} is already registered." - elif len(username) > 20: - err = "username can't be longer than 20 characters" - elif not username.isalnum(): - err = "username must be alphanumeric" + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] + db = get_db() + err = None + if not username: + err = "Username required" + elif not password: + err = "Password required" + elif db.execute( + "SELECT 1 FROM users WHERE username = ?;", (username,) + ).fetchone() is not None: + err = f"User {username} is already registered." + elif len(username) > 20: + err = "username can't be longer than 20 characters" + elif not username.isalnum(): + err = "username must be alphanumeric" - if err is None: - db.execute( - "INSERT INTO users (username, password, joined) VALUES (?,?,current_timestamp);", - (username,generate_password_hash(password)) - ) - db.commit() - session['user'] = username - if 'keep_logged_in' in request.form: - session['keep_logged_in'] = True - session.permanent = True - flash("successfully created account") - return redirect(get_next()) + if err is None: + db.execute( + "INSERT INTO users (username, password, joined) VALUES (?,?,current_timestamp);", + (username,generate_password_hash(password)) + ) + db.commit() + session['user'] = username + if 'keep_logged_in' in request.form: + session['keep_logged_in'] = True + session.permanent = True + flash("successfully created account") + return redirect(get_next()) - flash(err) - - return render_template("auth/register.html") + flash(err) + + return render_template("auth/register.html") @bp.route("/logout") def logout(): - session.clear() - flash("logged out successfully") - return redirect(get_next()) + session.clear() + flash("logged out successfully") + return redirect(get_next()) @bp.before_app_request def load_user(): - username = session.get("user") - if session.get("keep_logged_in",False): - session.permanent = True - if username is None: - g.user = None - g.user_info = None - else: - row = get_db().execute( - "SELECT * FROM users WHERE username = ?;", (username,) - ).fetchone() - if row is None: - g.user = None - g.user_info = None - else: - g.user = row['username'] - g.user_info = row - + username = session.get("user") + if session.get("keep_logged_in",False): + session.permanent = True + if username is None: + g.user = None + g.user_info = None + else: + row = get_db().execute( + "SELECT * FROM users WHERE username = ?;", (username,) + ).fetchone() + if row is None: + g.user = None + g.user_info = None + else: + g.user = row['username'] + g.user_info = row + def login_required(view): - @functools.wraps(view) - def wrapped(**kwargs): - print(g.user) - if g.user is None: - return redirect(url_for("auth.login")) - return view(**kwargs) - return wrapped + @functools.wraps(view) + def wrapped(**kwargs): + print(g.user) + if g.user is None: + return redirect(url_for("auth.login")) + return view(**kwargs) + return wrapped @bp.route("/cool") def cool(): - user = session.get("user") - if user is None: - return "you are not logged in" - else: - return f"you are logged in as {user}" + user = session.get("user") + if user is None: + return "you are not logged in" + else: + return f"you are logged in as {user}" @bp.route("/cooler") @login_required def cooler(): - return "bee" + return "bee" diff --git a/apioforum/csscolors.py b/apioforum/csscolors.py index 25bdfdd..c8c096d 100644 --- a/apioforum/csscolors.py +++ b/apioforum/csscolors.py @@ -1,150 +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" + "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 2d06a13..620749f 100644 --- a/apioforum/db.py +++ b/apioforum/db.py @@ -4,40 +4,40 @@ from flask import current_app, g from flask.cli import with_appcontext def get_db(): - if 'db' not in g: - g.db = sqlite3.connect( - current_app.config['DATABASE'], - detect_types=sqlite3.PARSE_DECLTYPES - ) - g.db.row_factory = sqlite3.Row - g.db.execute("PRAGMA foreign_keys = ON;") - return g.db + if 'db' not in g: + g.db = sqlite3.connect( + current_app.config['DATABASE'], + detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + g.db.execute("PRAGMA foreign_keys = ON;") + return g.db def close_db(e=None): - db = g.pop('db', None) - if db is not None: - db.close() + db = g.pop('db', None) + if db is not None: + db.close() migrations = [ """ CREATE TABLE users ( - username TEXT PRIMARY KEY, - password TEXT NOT NULL + username TEXT PRIMARY KEY, + password TEXT NOT NULL );""", """ CREATE TABLE threads ( - id INTEGER PRIMARY KEY, - title TEXT NOT NULL, - creator TEXT NOT NULL REFERENCES users(username), - created TIMESTAMP NOT NULL, - updated TIMESTAMP NOT NULL + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + creator TEXT NOT NULL REFERENCES users(username), + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL ); CREATE TABLE posts ( - id INTEGER PRIMARY KEY, - content TEXT, - thread INTEGER NOT NULL REFERENCES threads(id), - author TEXT NOT NULL REFERENCES users(username), - created TIMESTAMP NOT NULL + id INTEGER PRIMARY KEY, + content TEXT, + thread INTEGER NOT NULL REFERENCES threads(id), + author TEXT NOT NULL REFERENCES users(username), + created TIMESTAMP NOT NULL ); CREATE INDEX posts_thread_idx ON posts (thread); @@ -48,34 +48,34 @@ ALTER TABLE posts ADD COLUMN updated TIMESTAMP; """, """ CREATE VIRTUAL TABLE posts_fts USING fts5( - content, - content=posts, - content_rowid=id, - tokenize='porter unicode61 remove_diacritics 2' + content, + content=posts, + content_rowid=id, + tokenize='porter unicode61 remove_diacritics 2' ); INSERT INTO posts_fts (rowid, content) SELECT id, content FROM posts; CREATE TRIGGER posts_ai AFTER INSERT ON posts BEGIN - INSERT INTO posts_fts(rowid, content) VALUES (new.id, new.content); + INSERT INTO posts_fts(rowid, content) VALUES (new.id, new.content); END; CREATE TRIGGER posts_ad AFTER DELETE ON posts BEGIN - INSERT INTO posts_fts(posts_fts, rowid, content) VALUES('delete', old.id, old.content); + INSERT INTO posts_fts(posts_fts, rowid, content) VALUES('delete', old.id, old.content); END; CREATE TRIGGER posts_au AFTER UPDATE ON posts BEGIN - INSERT INTO posts_fts(posts_fts, rowid, content) VALUES('delete', old.id, old.content); - INSERT INTO posts_fts(rowid, content) VALUES (new.id, new.content); + INSERT INTO posts_fts(posts_fts, rowid, content) VALUES('delete', old.id, old.content); + INSERT INTO posts_fts(rowid, content) VALUES (new.id, new.content); END; """, """ CREATE TABLE tags ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - text_colour TEXT NOT NULL, - bg_colour TEXT NOT NULL + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + text_colour TEXT NOT NULL, + bg_colour TEXT NOT NULL ); CREATE TABLE thread_tags ( - thread INTEGER NOT NULL REFERENCES threads(id), - tag INTEGER NOT NULL REFERENCES tags(id) + thread INTEGER NOT NULL REFERENCES threads(id), + tag INTEGER NOT NULL REFERENCES tags(id) ); """, """CREATE INDEX thread_tags_thread ON thread_tags (thread);""", @@ -86,59 +86,59 @@ ALTER TABLE users ADD COLUMN joined TIMESTAMP; """, """ CREATE TABLE polls ( - id INTEGER PRIMARY KEY, - title TEXT NOT NULL + 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 ) + 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) + 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; + 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 + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + parent INTEGER REFERENCES forums(id), + description TEXT ); INSERT INTO forums (name,parent,description) values ('apioforum',null, - 'welcome to the apioforum\n\n' || - 'forum rules: do not be a bad person. do not do bad things.'); + 'welcome to the apioforum\n\n' || + 'forum rules: do not be a bad person. do not do bad things.'); PRAGMA foreign_keys = off; BEGIN TRANSACTION; CREATE TABLE threads_new ( - id INTEGER PRIMARY KEY, - title TEXT NOT NULL, - creator TEXT NOT NULL, - created TIMESTAMP NOT NULL, - updated TIMESTAMP NOT NULL, - forum NOT NULL REFERENCES forums(id), - poll INTEGER REFERENCES polls(id) + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + creator TEXT NOT NULL, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NOT NULL, + forum NOT NULL REFERENCES forums(id), + poll INTEGER REFERENCES polls(id) ); INSERT INTO threads_new (id,title,creator,created,updated,forum) - SELECT id,title,creator,created,updated,1 FROM threads; + SELECT id,title,creator,created,updated,1 FROM threads; DROP TABLE threads; ALTER TABLE threads_new RENAME TO threads; COMMIT; @@ -146,14 +146,14 @@ PRAGMA foreign_keys = on; """, """ CREATE VIEW most_recent_posts AS - SELECT max(id), * FROM posts GROUP BY thread; + SELECT max(id), * FROM posts GROUP BY thread; CREATE VIEW number_of_posts AS - SELECT thread, count(*) AS num_replies FROM posts GROUP BY thread; + 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; + SELECT poll, count(*) AS total_votes FROM votes WHERE current AND NOT is_retraction GROUP BY poll; """, """ PRAGMA foreign_keys = off; @@ -173,19 +173,19 @@ PRAGMA foreign_keys = on; """, """ CREATE TABLE role_config ( - role TEXT NOT NULL, - forum NOT NULL REFERENCES forums(id), - id INTEGER PRIMARY KEY, + role TEXT NOT NULL, + forum NOT NULL REFERENCES forums(id), + id INTEGER PRIMARY KEY, - p_create_threads INT NOT NULL DEFAULT 1, - p_reply_threads INT NOT NULL DEFAULT 1, - p_view_threads INT NOT NULL DEFAULT 1, - p_manage_threads INT NOT NULL DEFAULT 0, - p_delete_posts INT NOT NULL DEFAULT 0, - p_vote INT NOT NULL DEFAULT 1, - p_create_polls INT NOT NULL DEFAULT 1, - p_approve INT NOT NULL DEFAULT 0, - p_create_subforum INT NOT NULL DEFAULT 0 + p_create_threads INT NOT NULL DEFAULT 1, + p_reply_threads INT NOT NULL DEFAULT 1, + p_view_threads INT NOT NULL DEFAULT 1, + p_manage_threads INT NOT NULL DEFAULT 0, + p_delete_posts INT NOT NULL DEFAULT 0, + p_vote INT NOT NULL DEFAULT 1, + p_create_polls INT NOT NULL DEFAULT 1, + p_approve INT NOT NULL DEFAULT 0, + p_create_subforum INT NOT NULL DEFAULT 0 ); INSERT INTO role_config (role,forum) VALUES ("approved",1); @@ -193,9 +193,9 @@ INSERT INTO role_config (role,forum) VALUES ("other",1); """, """ CREATE TABLE role_assignments ( - user NOT NULL REFERENCES users(username), - forum NOT NULL REFERENCES forums(id), - role TEXT NOT NULL + user NOT NULL REFERENCES users(username), + forum NOT NULL REFERENCES forums(id), + role TEXT NOT NULL ); """, """ @@ -209,36 +209,36 @@ ALTER TABLE role_config ADD COLUMN p_view_forum INT NOT NULL DEFAULT 1; """, """ CREATE TABLE webhooks ( - id INTEGER PRIMARY KEY, - type TEXT NOT NULL, - url TEXT NOT NULL, - forum INTEGER NOT NULL REFERENCES forums(id) + id INTEGER PRIMARY KEY, + type TEXT NOT NULL, + url TEXT NOT NULL, + forum INTEGER NOT NULL REFERENCES forums(id) );""", """ CREATE VIEW public_forums AS - SELECT f.id as id, - COALESCE(r.p_view_forum,1) as public - FROM forums f - LEFT JOIN role_config r ON - r.forum = f.id AND r.role='other'; + SELECT f.id as id, + COALESCE(r.p_view_forum,1) as public + FROM forums f + LEFT JOIN role_config r ON + r.forum = f.id AND r.role='other'; CREATE VIEW forum_thread_of_post AS - SELECT p.id as p_id, t.id as t_id, f.id as f_id - FROM posts p - JOIN threads t on p.thread = t.id - JOIN forums f on t.forum = f.id; + SELECT p.id as p_id, t.id as t_id, f.id as f_id + FROM posts p + JOIN threads t on p.thread = t.id + JOIN forums f on t.forum = f.id; CREATE VIEW public_posts AS - SELECT p.id AS id, - b.public AS public - FROM posts p - JOIN forum_thread_of_post h ON p.id=h.p_id - JOIN public_forums b ON b.id=h.f_id; + SELECT p.id AS id, + b.public AS public + FROM posts p + JOIN forum_thread_of_post h ON p.id=h.p_id + JOIN public_forums b ON b.id=h.f_id; """, """ CREATE TABLE read ( - user NOT NULL REFERENCES users(username), - forum REFERENCES forums(id), - thread REFERENCES threads(id), - time TIMESTAMP NOT NULL + user NOT NULL REFERENCES users(username), + forum REFERENCES forums(id), + thread REFERENCES threads(id), + time TIMESTAMP NOT NULL ); ALTER TABLE forums ADD COLUMN updated TIMESTAMP; """, @@ -246,22 +246,22 @@ ALTER TABLE forums ADD COLUMN updated TIMESTAMP; def init_db(): - db = get_db() - version = db.execute("PRAGMA user_version;").fetchone()[0] - for i in range(version, len(migrations)): - db.executescript(migrations[i]) - db.execute(f"PRAGMA user_version = {i+1}") - db.commit() - click.echo(f"migration {i}") + db = get_db() + version = db.execute("PRAGMA user_version;").fetchone()[0] + for i in range(version, len(migrations)): + db.executescript(migrations[i]) + db.execute(f"PRAGMA user_version = {i+1}") + db.commit() + click.echo(f"migration {i}") @click.command("migrate") @with_appcontext def migrate_command(): - """update database scheme etc""" - init_db() - click.echo("ok") + """update database scheme etc""" + init_db() + click.echo("ok") def init_app(app): - app.teardown_appcontext(close_db) - app.cli.add_command(migrate_command) + app.teardown_appcontext(close_db) + app.cli.add_command(migrate_command) diff --git a/apioforum/forum.py b/apioforum/forum.py index b86fcc9..ce90853 100644 --- a/apioforum/forum.py +++ b/apioforum/forum.py @@ -3,8 +3,8 @@ # ^ aha we never removed this. we should keep it. it is funny. from flask import ( - Blueprint, render_template, request, - g, redirect, url_for, flash, abort + Blueprint, render_template, request, + g, redirect, url_for, flash, abort ) from .db import get_db @@ -23,484 +23,484 @@ bp = Blueprint("forum", __name__, url_prefix="/") @bp.route("/") def not_actual_index(): - return redirect("/1") + 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 + 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(""" - WITH RECURSIVE fs AS - (SELECT * FROM forums WHERE id = ? - UNION ALL - SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id) - SELECT * FROM fs; - """,(forum_id,)).fetchall() - ancestors.reverse() - return ancestors + db = get_db() + ancestors = db.execute(""" + WITH RECURSIVE fs AS + (SELECT * FROM forums WHERE id = ? + UNION ALL + SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id) + SELECT * FROM fs; + """,(forum_id,)).fetchall() + ancestors.reverse() + return ancestors def forum_route(relative_path, pagination=False, **kwargs): - def decorator(f): - path = "/<int:forum_id>" - if relative_path != "": - path += "/" + relative_path - - @bp.route(path, **kwargs) - @functools.wraps(f) - def wrapper(forum_id, *args, **kwargs): - db = get_db() - forum = db.execute("SELECT * FROM forums WHERE id = ?", - (forum_id,)).fetchone() - if forum == None: - abort(404) - return f(forum, *args, **kwargs) - - if pagination: - wrapper = bp.route(path+"/page/<int:page>", **kwargs)(wrapper) - - return decorator + def decorator(f): + path = "/<int:forum_id>" + if relative_path != "": + path += "/" + relative_path + + @bp.route(path, **kwargs) + @functools.wraps(f) + def wrapper(forum_id, *args, **kwargs): + db = get_db() + forum = db.execute("SELECT * FROM forums WHERE id = ?", + (forum_id,)).fetchone() + if forum == None: + abort(404) + return f(forum, *args, **kwargs) + + if pagination: + wrapper = bp.route(path+"/page/<int:page>", **kwargs)(wrapper) + + return decorator def requires_permission(permission, login_required=True): - def decorator(f): - @functools.wraps(f) - def wrapper(forum, *args, **kwargs): - if not has_permission(forum['id'],g.user,permission,login_required): - abort(403) - return f(forum, *args, **kwargs) - return wrapper - return decorator + def decorator(f): + @functools.wraps(f) + def wrapper(forum, *args, **kwargs): + if not has_permission(forum['id'],g.user,permission,login_required): + abort(403) + return f(forum, *args, **kwargs) + return wrapper + return decorator def requires_bureaucrat(f): - @functools.wraps(f) - @requires_permission("p_view_forum") - def wrapper(forum, *args, **kwargs): - if not is_bureaucrat(forum['id'], g.user): - abort(403) - return f(forum, *args, **kwargs) - return wrapper + @functools.wraps(f) + @requires_permission("p_view_forum") + def wrapper(forum, *args, **kwargs): + if not is_bureaucrat(forum['id'], g.user): + abort(403) + return f(forum, *args, **kwargs) + return wrapper def set_updated(forum_id): - db = get_db() - ancestors = db.execute(""" - WITH RECURSIVE fs AS - (SELECT * FROM forums WHERE id = ? - UNION ALL - SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id) - SELECT * FROM fs; - """,(forum_id,)).fetchall() - for f in ancestors: - db.execute("UPDATE forums SET updated = current_timestamp WHERE id = ?;", (f['id'],)) - db.commit() + db = get_db() + ancestors = db.execute(""" + WITH RECURSIVE fs AS + (SELECT * FROM forums WHERE id = ? + UNION ALL + SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id) + SELECT * FROM fs; + """,(forum_id,)).fetchall() + for f in ancestors: + db.execute("UPDATE forums SET updated = current_timestamp WHERE id = ?;", (f['id'],)) + db.commit() @forum_route("",pagination=True) @requires_permission("p_view_forum", login_required=False) def view_forum(forum,page=1): - db = get_db() - - sortby = request.args.get("sortby","ad") - try: - sortby_dir = {'d':'DESC','a':'ASC'}[sortby[1]] - sortby_by = {'a':'threads.updated','c':'threads.created'}[sortby[0]] - except KeyError: - return redirect(url_for('forum.view_forum',forum_id=forum['id'])) - - avail_tags = get_avail_tags(forum['id']) - - tagfilter = request.args.get("tagfilter",None) - if tagfilter == "": - tagfilter = None - tagfilter_clause = "" - tagfilter_tag = None - if tagfilter is not None: - try: - tagfilter = int(tagfilter) - except ValueError: - flash(f'invalid tag id "{tagfilter}"') - return redirect(url_for('forum.view_forum',forum_id=forum['id'])) - else: - # there is no risk of sql injection because - # we just checked it is an int - tagfilter_clause = f"AND thread_tags.tag = {tagfilter}" - for the_tag in avail_tags: - if the_tag['id'] == tagfilter: - tagfilter_tag = the_tag - break - else: - flash("that tag doesn't exist or isn't available here") - return redirect(url_for('forum.view_forum',forum_id=forum['id'])) - - - threads = db.execute( - f"""SELECT - threads.id, threads.title, threads.creator, threads.created, - 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, - most_recent_posts.content as mrp_content, - most_recent_posts.deleted as mrp_deleted - FROM threads - INNER JOIN most_recent_posts ON most_recent_posts.thread = threads.id - INNER JOIN number_of_posts ON number_of_posts.thread = threads.id - LEFT OUTER JOIN thread_tags ON threads.id = thread_tags.thread - WHERE threads.forum = ? {tagfilter_clause} - GROUP BY threads.id - ORDER BY {sortby_by} {sortby_dir} - LIMIT ? OFFSET ?; - """,( - forum['id'], - THREADS_PER_PAGE, - (page-1)*THREADS_PER_PAGE, - )).fetchall() - - num_threads = db.execute(f""" - SELECT count(*) AS count FROM threads - LEFT OUTER JOIN thread_tags ON threads.id = thread_tags.thread - WHERE threads.forum = ? {tagfilter_clause}; - """,(forum['id'],)).fetchone()['count'] - max_pageno = math.ceil(num_threads/THREADS_PER_PAGE) - if page < 1: - abort(404) - elif page > max_pageno and (max_pageno > 0 or page != 1): - abort(404) - - thread_tags = {} - thread_polls = {} - - - #todo: somehow optimise this - for thread in threads: - thread_tags[thread['id']] = 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() - - 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 - WHERE parent = ? AND unlisted = 0 - GROUP BY forums.id - ORDER BY name ASC - """,(forum['id'],)).fetchall() - subforums = [] - for s in subforums_rows: - a={} - a.update(s) - if a['updated'] is not None: - a['updated'] = datetime.datetime.fromisoformat(a['updated']) - if has_permission(a['id'],g.user,"p_view_forum",login_required=False): - subforums.append(a) - - bureaucrats = db.execute(""" - SELECT user FROM role_assignments - WHERE role = 'bureaucrat' AND forum = ? - """,(forum['id'],)).fetchall() - bureaucrats = [b[0] for b in bureaucrats] - - - if g.user != None: - db.execute("DELETE FROM read WHERE user = ? AND forum = ?;", (g.user, forum['id'])) - db.execute("INSERT INTO read (user,forum,time) VALUES (?,?,current_timestamp);", (g.user, forum['id'])) - db.commit() - - - return render_template("view_forum.html", - forum=forum, - subforums=subforums, - threads=threads, - thread_tags=thread_tags, - bureaucrats=bureaucrats, - thread_polls=thread_polls, - avail_tags=avail_tags, - max_pageno=max_pageno, - page=page, - current_sortby=sortby, - tagfilter_tag=tagfilter_tag, - is_read=read.is_read, - ) + db = get_db() + + sortby = request.args.get("sortby","ad") + try: + sortby_dir = {'d':'DESC','a':'ASC'}[sortby[1]] + sortby_by = {'a':'threads.updated','c':'threads.created'}[sortby[0]] + except KeyError: + return redirect(url_for('forum.view_forum',forum_id=forum['id'])) + + avail_tags = get_avail_tags(forum['id']) + + tagfilter = request.args.get("tagfilter",None) + if tagfilter == "": + tagfilter = None + tagfilter_clause = "" + tagfilter_tag = None + if tagfilter is not None: + try: + tagfilter = int(tagfilter) + except ValueError: + flash(f'invalid tag id "{tagfilter}"') + return redirect(url_for('forum.view_forum',forum_id=forum['id'])) + else: + # there is no risk of sql injection because + # we just checked it is an int + tagfilter_clause = f"AND thread_tags.tag = {tagfilter}" + for the_tag in avail_tags: + if the_tag['id'] == tagfilter: + tagfilter_tag = the_tag + break + else: + flash("that tag doesn't exist or isn't available here") + return redirect(url_for('forum.view_forum',forum_id=forum['id'])) + + + threads = db.execute( + f"""SELECT + threads.id, threads.title, threads.creator, threads.created, + 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, + most_recent_posts.content as mrp_content, + most_recent_posts.deleted as mrp_deleted + FROM threads + INNER JOIN most_recent_posts ON most_recent_posts.thread = threads.id + INNER JOIN number_of_posts ON number_of_posts.thread = threads.id + LEFT OUTER JOIN thread_tags ON threads.id = thread_tags.thread + WHERE threads.forum = ? {tagfilter_clause} + GROUP BY threads.id + ORDER BY {sortby_by} {sortby_dir} + LIMIT ? OFFSET ?; + """,( + forum['id'], + THREADS_PER_PAGE, + (page-1)*THREADS_PER_PAGE, + )).fetchall() + + num_threads = db.execute(f""" + SELECT count(*) AS count FROM threads + LEFT OUTER JOIN thread_tags ON threads.id = thread_tags.thread + WHERE threads.forum = ? {tagfilter_clause}; + """,(forum['id'],)).fetchone()['count'] + max_pageno = math.ceil(num_threads/THREADS_PER_PAGE) + if page < 1: + abort(404) + elif page > max_pageno and (max_pageno > 0 or page != 1): + abort(404) + + thread_tags = {} + thread_polls = {} + + + #todo: somehow optimise this + for thread in threads: + thread_tags[thread['id']] = 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() + + 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 + WHERE parent = ? AND unlisted = 0 + GROUP BY forums.id + ORDER BY name ASC + """,(forum['id'],)).fetchall() + subforums = [] + for s in subforums_rows: + a={} + a.update(s) + if a['updated'] is not None: + a['updated'] = datetime.datetime.fromisoformat(a['updated']) + if has_permission(a['id'],g.user,"p_view_forum",login_required=False): + subforums.append(a) + + bureaucrats = db.execute(""" + SELECT user FROM role_assignments + WHERE role = 'bureaucrat' AND forum = ? + """,(forum['id'],)).fetchall() + bureaucrats = [b[0] for b in bureaucrats] + + + if g.user != None: + db.execute("DELETE FROM read WHERE user = ? AND forum = ?;", (g.user, forum['id'])) + db.execute("INSERT INTO read (user,forum,time) VALUES (?,?,current_timestamp);", (g.user, forum['id'])) + db.commit() + + + return render_template("view_forum.html", + forum=forum, + subforums=subforums, + threads=threads, + thread_tags=thread_tags, + bureaucrats=bureaucrats, + thread_polls=thread_polls, + avail_tags=avail_tags, + max_pageno=max_pageno, + page=page, + current_sortby=sortby, + tagfilter_tag=tagfilter_tag, + is_read=read.is_read, + ) @forum_route("create_thread",methods=("GET","POST")) @requires_permission("p_create_threads") @requires_permission("p_view_forum") def create_thread(forum): - db = get_db() - forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum['id'],)).fetchone() - if forum is None: - flash("that forum doesn't exist") - return redirect(url_for('index')) - - if g.user is None: - flash("you need to be logged in to create a thread") - return redirect(url_for('index')) - - if request.method == "POST": - title = request.form['title'] - content = request.form['content'] - err = None - if len(title.strip()) == 0 or len(content.strip()) == 0: - err = "title and content can't be empty" - - if err is None: - cur = db.cursor() - cur.execute( - "INSERT INTO threads (title,creator,created,updated,forum) VALUES (?,?,current_timestamp,current_timestamp,?);", - (title,g.user,forum['id']) - ) - thread_id = cur.lastrowid - cur.execute( - "INSERT INTO posts (thread,created,author,content) VALUES (?,current_timestamp,?,?);", - (thread_id,g.user,content) - ) - db.commit() - set_updated(forum['id']) - - from . import webhooks - thread = db.execute("select * from threads where id = ?",(thread_id,)).fetchone() - webhooks.do_webhooks_thread(forum['id'],thread) - return redirect(url_for('thread.view_thread',thread_id=thread_id)) - flash(err) - - - return render_template("create_thread.html") + db = get_db() + forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum['id'],)).fetchone() + if forum is None: + flash("that forum doesn't exist") + return redirect(url_for('index')) + + if g.user is None: + flash("you need to be logged in to create a thread") + return redirect(url_for('index')) + + if request.method == "POST": + title = request.form['title'] + content = request.form['content'] + err = None + if len(title.strip()) == 0 or len(content.strip()) == 0: + err = "title and content can't be empty" + + if err is None: + cur = db.cursor() + cur.execute( + "INSERT INTO threads (title,creator,created,updated,forum) VALUES (?,?,current_timestamp,current_timestamp,?);", + (title,g.user,forum['id']) + ) + thread_id = cur.lastrowid + cur.execute( + "INSERT INTO posts (thread,created,author,content) VALUES (?,current_timestamp,?,?);", + (thread_id,g.user,content) + ) + db.commit() + set_updated(forum['id']) + + from . import webhooks + thread = db.execute("select * from threads where id = ?",(thread_id,)).fetchone() + webhooks.do_webhooks_thread(forum['id'],thread) + return redirect(url_for('thread.view_thread',thread_id=thread_id)) + flash(err) + + + return render_template("create_thread.html") @forum_route("roles",methods=("GET","POST")) @requires_bureaucrat def edit_roles(forum): - db = get_db() - role_configs = db.execute( - "SELECT * FROM role_config WHERE forum = ? ORDER BY ID ASC", - (forum['id'],)).fetchall() - - if request.method == "POST": - for config in role_configs: - if 'delete_' + config['role'] in request.form: - db.execute( - "DELETE FROM role_config WHERE forum = ? AND role = ?", - (forum['id'],config['role'])) - elif 'roleconfig_' + config['role'] in request.form: - for p in role_permissions: - permission_setting =\ - f"perm_{config['role']}_{p}" in request.form - db.execute(f""" - UPDATE role_config SET {p} = ? - WHERE forum = ? AND role = ?; - """, - (permission_setting,forum['id'], config['role'])) - db.commit() - flash('roles sucessfully enroled') - return redirect(url_for('forum.view_forum',forum_id=forum['id'])) - - role_config_roles = [c['role'] for c in role_configs] - other_roles = [role for role in get_forum_roles(forum['id']) if not role in role_config_roles] - - return render_template("edit_permissions.html", - forum=forum, - role_configs=role_configs, - other_roles=other_roles - ) + db = get_db() + role_configs = db.execute( + "SELECT * FROM role_config WHERE forum = ? ORDER BY ID ASC", + (forum['id'],)).fetchall() + + if request.method == "POST": + for config in role_configs: + if 'delete_' + config['role'] in request.form: + db.execute( + "DELETE FROM role_config WHERE forum = ? AND role = ?", + (forum['id'],config['role'])) + elif 'roleconfig_' + config['role'] in request.form: + for p in role_permissions: + permission_setting =\ + f"perm_{config['role']}_{p}" in request.form + db.execute(f""" + UPDATE role_config SET {p} = ? + WHERE forum = ? AND role = ?; + """, + (permission_setting,forum['id'], config['role'])) + db.commit() + flash('roles sucessfully enroled') + return redirect(url_for('forum.view_forum',forum_id=forum['id'])) + + role_config_roles = [c['role'] for c in role_configs] + other_roles = [role for role in get_forum_roles(forum['id']) if not role in role_config_roles] + + return render_template("edit_permissions.html", + forum=forum, + role_configs=role_configs, + other_roles=other_roles + ) @forum_route("roles/new",methods=["POST"]) @requires_bureaucrat def add_role(forum): - name = request.form['role'].strip() - if not all(c in (" ","-","_") or c.isalnum() for c in name) \ - or len(name) > 32: - flash("role name must contain no special characters") - return redirect(url_for('forum.edit_roles',forum_id=forum['id'])) - if name == "bureaucrat": - flash("cannot configure permissions for bureaucrat") - return redirect(url_for('forum.edit_roles',forum_id=forum['id'])) - - db = get_db() - - existing_config = db.execute(""" - SELECT * FROM role_config WHERE forum = ? AND role = ? - """,(forum['id'],name)).fetchone() - if not existing_config: - db.execute("INSERT INTO role_config (forum,role) VALUES (?,?)", - (forum['id'],name)) - db.commit() - return redirect(url_for('forum.edit_roles',forum_id=forum['id'])) + name = request.form['role'].strip() + if not all(c in (" ","-","_") or c.isalnum() for c in name) \ + or len(name) > 32: + flash("role name must contain no special characters") + return redirect(url_for('forum.edit_roles',forum_id=forum['id'])) + if name == "bureaucrat": + flash("cannot configure permissions for bureaucrat") + return redirect(url_for('forum.edit_roles',forum_id=forum['id'])) + + db = get_db() + + existing_config = db.execute(""" + SELECT * FROM role_config WHERE forum = ? AND role = ? + """,(forum['id'],name)).fetchone() + if not existing_config: + db.execute("INSERT INTO role_config (forum,role) VALUES (?,?)", + (forum['id'],name)) + db.commit() + return redirect(url_for('forum.edit_roles',forum_id=forum['id'])) @forum_route("role",methods=["GET","POST"]) @requires_permission("p_approve") def view_user_role(forum): - if request.method == "POST": - return redirect(url_for( 'forum.edit_user_role', - username=request.form['user'],forum_id=forum['id'])) - else: - return render_template("role_assignment.html",forum=forum) + if request.method == "POST": + return redirect(url_for( 'forum.edit_user_role', + username=request.form['user'],forum_id=forum['id'])) + else: + return render_template("role_assignment.html",forum=forum) @forum_route("role/<username>",methods=["GET","POST"]) @requires_permission("p_approve") def edit_user_role(forum, username): - db = get_db() - if request.method == "POST": - user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone() - if user == None: - return redirect(url_for('forum.edit_user_role', - username=username,forum_id=forum['id'])) - role = request.form['role'] - if role not in get_forum_roles(forum['id']) and role != "" and role != "bureaucrat": - flash("no such role") - return redirect(url_for('forum.edit_user_role', - username=username,forum_id=forum['id'])) - if not is_bureaucrat(forum['id'],g.user) and role != "approved" and role != "": - # only bureaucrats can assign arbitrary roles - abort(403) - existing = db.execute( - "SELECT * FROM role_assignments WHERE user = ? AND forum = ?;", - (username,forum['id'])).fetchone() - if existing: - db.execute("DELETE FROM role_assignments WHERE user = ? AND forum = ?;",(username,forum['id'])) - if role != "": - db.execute( - "INSERT INTO role_assignments (user,role,forum) VALUES (?,?,?);", - (username,role,forum['id'])) - db.commit() - flash("role assigned assignedly") - return redirect(url_for('forum.view_forum',forum_id=forum['id'])) - else: - user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone() - if user == None: - return render_template("role_assignment.html", - forum=forum,user=username,invalid_user=True) - r = db.execute( - "SELECT role FROM role_assignments WHERE user = ? AND forum = ?;", - (username,forum['id'])).fetchone() - if not r: - assigned_role = "" - else: - assigned_role = r[0] - role = get_user_role(forum['id'], username) - if is_bureaucrat(forum['id'], g.user): - roles = get_forum_roles(forum['id']) - roles.remove("other") - roles.add("bureaucrat") - else: - roles = ["approved"] - return render_template("role_assignment.html", - forum=forum,user=username,role=role, - assigned_role=assigned_role,forum_roles=roles) + db = get_db() + if request.method == "POST": + user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone() + if user == None: + return redirect(url_for('forum.edit_user_role', + username=username,forum_id=forum['id'])) + role = request.form['role'] + if role not in get_forum_roles(forum['id']) and role != "" and role != "bureaucrat": + flash("no such role") + return redirect(url_for('forum.edit_user_role', + username=username,forum_id=forum['id'])) + if not is_bureaucrat(forum['id'],g.user) and role != "approved" and role != "": + # only bureaucrats can assign arbitrary roles + abort(403) + existing = db.execute( + "SELECT * FROM role_assignments WHERE user = ? AND forum = ?;", + (username,forum['id'])).fetchone() + if existing: + db.execute("DELETE FROM role_assignments WHERE user = ? AND forum = ?;",(username,forum['id'])) + if role != "": + db.execute( + "INSERT INTO role_assignments (user,role,forum) VALUES (?,?,?);", + (username,role,forum['id'])) + db.commit() + flash("role assigned assignedly") + return redirect(url_for('forum.view_forum',forum_id=forum['id'])) + else: + user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone() + if user == None: + return render_template("role_assignment.html", + forum=forum,user=username,invalid_user=True) + r = db.execute( + "SELECT role FROM role_assignments WHERE user = ? AND forum = ?;", + (username,forum['id'])).fetchone() + if not r: + assigned_role = "" + else: + assigned_role = r[0] + role = get_user_role(forum['id'], username) + if is_bureaucrat(forum['id'], g.user): + roles = get_forum_roles(forum['id']) + roles.remove("other") + roles.add("bureaucrat") + else: + roles = ["approved"] + return render_template("role_assignment.html", + forum=forum,user=username,role=role, + assigned_role=assigned_role,forum_roles=roles) def forum_config_page(forum, create=False): - db = get_db() - if request.method == "POST": - name = request.form["name"] - desc = request.form["description"] - if len(name) > 100 or len(name.strip()) == 0: - flash("invalid name") - return redirect(url_for('forum.edit_forum',forum_id=forum['id'])) - elif len(desc) > 6000: - flash("invalid description") - return redirect(url_for('forum.edit_forum',forum_id=forum['id'])) - if not create: - db.execute("UPDATE forums SET name = ?, description = ? WHERE id = ?", - (name,desc,forum['id'])) - fid = forum['id'] - else: - cur = db.cursor() - cur.execute( - "INSERT INTO forums (name,description,parent) VALUES (?,?,?)", - (name,desc,forum['id'])) - new = cur.lastrowid - # creator becomes bureaucrat of new forum - db.execute("INSERT INTO role_assignments (role,user,forum) VALUES (?,?,?)", - ("bureaucrat",g.user,new)) - fid = new - db.commit() - return redirect(url_for('forum.view_forum',forum_id=fid)) - else: - if create: - name = "" - desc = "" - else: - name = forum['name'] - desc = forum['description'] - cancel_link = url_for('forum.view_forum',forum_id=forum['id']) - return render_template("edit_forum.html",create=create, - name=name,description=desc,cancel_link=cancel_link) + db = get_db() + if request.method == "POST": + name = request.form["name"] + desc = request.form["description"] + if len(name) > 100 or len(name.strip()) == 0: + flash("invalid name") + return redirect(url_for('forum.edit_forum',forum_id=forum['id'])) + elif len(desc) > 6000: + flash("invalid description") + return redirect(url_for('forum.edit_forum',forum_id=forum['id'])) + if not create: + db.execute("UPDATE forums SET name = ?, description = ? WHERE id = ?", + (name,desc,forum['id'])) + fid = forum['id'] + else: + cur = db.cursor() + cur.execute( + "INSERT INTO forums (name,description,parent) VALUES (?,?,?)", + (name,desc,forum['id'])) + new = cur.lastrowid + # creator becomes bureaucrat of new forum + db.execute("INSERT INTO role_assignments (role,user,forum) VALUES (?,?,?)", + ("bureaucrat",g.user,new)) + fid = new + db.commit() + return redirect(url_for('forum.view_forum',forum_id=fid)) + else: + if create: + name = "" + desc = "" + else: + name = forum['name'] + desc = forum['description'] + cancel_link = url_for('forum.view_forum',forum_id=forum['id']) + return render_template("edit_forum.html",create=create, + name=name,description=desc,cancel_link=cancel_link) @forum_route("edit",methods=["GET","POST"]) @requires_bureaucrat def edit_forum(forum): - return forum_config_page(forum) + return forum_config_page(forum) @forum_route("create",methods=["GET","POST"]) @requires_permission("p_create_subforum") def create_forum(forum): - return forum_config_page(forum,create=True) + return forum_config_page(forum,create=True) #@forum_route("unlisted") #def view_unlisted(forum): -# if not is_admin: abort(403) # why doesn't this fucking work -# db = get_db() -# unlisted = db.execute( -# "SELECT * FROM forums WHERE unlisted = 1 AND parent = ?",(forum['id'],)) -# return render_template('view_unlisted.html',forum=forum,unlisted=unlisted) +# if not is_admin: abort(403) # why doesn't this fucking work +# db = get_db() +# unlisted = db.execute( +# "SELECT * FROM forums WHERE unlisted = 1 AND parent = ?",(forum['id'],)) +# return render_template('view_unlisted.html',forum=forum,unlisted=unlisted) @bp.route("/search") def search(): - db = get_db() - query = request.args["q"] - try: - results = db.execute(""" - SELECT posts.id, highlight(posts_fts, 0, '<mark>', '</mark>') AS - content, posts.thread, posts.author, posts.created, posts.edited, - posts.updated, threads.title AS thread_title - FROM posts_fts - JOIN posts ON posts_fts.rowid = posts.id - JOIN threads ON threads.id = posts.thread - JOIN public_posts ON public_posts.id = posts.id - WHERE posts_fts MATCH ? AND public_posts.public - ORDER BY rank - LIMIT 50 - """, (query,)).fetchall() - except OperationalError as e: - print(e) - flash('your search query was malformed.') - return redirect(url_for("forum.not_actual_index")) - - display_thread_id = [ True ] * len(results) - last_thread = None - for ix, result in enumerate(results): - if result["thread"] == last_thread: - display_thread_id[ix] = False - last_thread = result["thread"] - return render_template("search_results.html", results=results, query=query, display_thread_id=display_thread_id) + db = get_db() + query = request.args["q"] + try: + results = db.execute(""" + SELECT posts.id, highlight(posts_fts, 0, '<mark>', '</mark>') AS + content, posts.thread, posts.author, posts.created, posts.edited, + posts.updated, threads.title AS thread_title + FROM posts_fts + JOIN posts ON posts_fts.rowid = posts.id + JOIN threads ON threads.id = posts.thread + JOIN public_posts ON public_posts.id = posts.id + WHERE posts_fts MATCH ? AND public_posts.public + ORDER BY rank + LIMIT 50 + """, (query,)).fetchall() + except OperationalError as e: + print(e) + flash('your search query was malformed.') + return redirect(url_for("forum.not_actual_index")) + + display_thread_id = [ True ] * len(results) + last_thread = None + for ix, result in enumerate(results): + if result["thread"] == last_thread: + display_thread_id[ix] = False + last_thread = result["thread"] + return render_template("search_results.html", results=results, query=query, display_thread_id=display_thread_id) diff --git a/apioforum/fuzzy.py b/apioforum/fuzzy.py index 8396b8f..6edb649 100644 --- a/apioforum/fuzzy.py +++ b/apioforum/fuzzy.py @@ -1,35 +1,35 @@ # fuzzy datetime things units = ( - ("y", "year","years",365*24*60*60), # leap years aren't real - ("d", "day","days",24*60*60), - ("h", "hour","hours",60*60), - ("m", "minute","minutes",60), - ("s", "second","seconds",1), + ("y", "year","years",365*24*60*60), # leap years aren't real + ("d", "day","days",24*60*60), + ("h", "hour","hours",60*60), + ("m", "minute","minutes",60), + ("s", "second","seconds",1), ) from datetime import datetime, timedelta, timezone def fuzzy(seconds, ago=False): - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - elif isinstance(seconds, datetime): - seconds = (seconds.replace(tzinfo=timezone.utc) - datetime.now(tz=timezone.utc)).total_seconds() + if isinstance(seconds, timedelta): + seconds = seconds.total_seconds() + elif isinstance(seconds, datetime): + seconds = (seconds.replace(tzinfo=timezone.utc) - datetime.now(tz=timezone.utc)).total_seconds() - components_used = 0 - fmt = "{}" - buf = "" - if ago: - fmt = "in {}" if seconds > 0 else "{} ago" - elif seconds > 0: fmt = "in {}" - seconds = abs(seconds) - for short, _, _, unit_length in units: - if seconds >= unit_length: - components_used += 1 - qty = seconds // unit_length - buf += str(int(qty)) + short - seconds -= qty * unit_length - if components_used == 2: break - if not buf: return "now" + components_used = 0 + fmt = "{}" + buf = "" + if ago: + fmt = "in {}" if seconds > 0 else "{} ago" + elif seconds > 0: fmt = "in {}" + seconds = abs(seconds) + for short, _, _, unit_length in units: + if seconds >= unit_length: + components_used += 1 + qty = seconds // unit_length + buf += str(int(qty)) + short + seconds -= qty * unit_length + if components_used == 2: break + if not buf: return "now" - return fmt.format(buf) + return fmt.format(buf) diff --git a/apioforum/mdrender.py b/apioforum/mdrender.py index d9b8ea1..5a50661 100644 --- a/apioforum/mdrender.py +++ b/apioforum/mdrender.py @@ -2,26 +2,26 @@ import bleach from .csscolors import csscolors allowed_tags = [ - 'p', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'pre', - 'del', - 'ins', - 'mark', - 'img', - 'marquee', - 'pulsate', - 'sup','sub', - 'table','thead','tbody','tr','th','td', - 'details','summary', - 'hr', - 'br', - + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'pre', + 'del', + 'ins', + 'mark', + 'img', + 'marquee', + 'pulsate', + 'sup','sub', + 'table','thead','tbody','tr','th','td', + 'details','summary', + 'hr', + 'br', + ] @@ -31,9 +31,9 @@ allowed_tags += ("mark" + c for c in csscolors) allowed_attributes = bleach.sanitizer.ALLOWED_ATTRIBUTES.copy() allowed_attributes.update( - img=['src','alt','title'], - ol=['start'], - details=['open'], + img=['src','alt','title'], + ol=['start'], + details=['open'], ) allowed_tags.extend(bleach.sanitizer.ALLOWED_TAGS) @@ -42,14 +42,14 @@ cleaner = bleach.sanitizer.Cleaner(tags=allowed_tags,attributes=allowed_attribut import markdown md = markdown.Markdown(extensions=[ - 'pymdownx.tilde', - 'pymdownx.caret', - 'fenced_code', - 'tables', - 'pymdownx.details', + 'pymdownx.tilde', + 'pymdownx.caret', + 'fenced_code', + 'tables', + 'pymdownx.details', ]) def render(text): - text = md.reset().convert(text) - text = cleaner.clean(text) - return text + text = md.reset().convert(text) + text = cleaner.clean(text) + return text diff --git a/apioforum/permissions.py b/apioforum/permissions.py index 816936c..f705d08 100644 --- a/apioforum/permissions.py +++ b/apioforum/permissions.py @@ -1,5 +1,5 @@ from flask import ( - g, redirect, url_for, flash + g, redirect, url_for, flash ) import functools import click @@ -7,37 +7,37 @@ from flask.cli import with_appcontext from .db import get_db def is_admin(): - if g.user_info is None: - return False - else: - return g.user_info['admin'] > 0 + if g.user_info is None: + return False + else: + return g.user_info['admin'] > 0 def admin_required(view): - @functools.wraps(view) - def wrapped(**kwargs): - if is_admin(): - return view(**kwargs) - else: - flash("you must be an admin to do that") - return redirect(url_for("index")) - return wrapped + @functools.wraps(view) + def wrapped(**kwargs): + if is_admin(): + return view(**kwargs) + else: + flash("you must be an admin to do that") + return redirect(url_for("index")) + return wrapped @click.command("make_admin") @click.argument("username") @with_appcontext def make_admin(username): - """makes a user an admin user""" - db = get_db() - cur = db.cursor() - cur.execute("UPDATE users SET admin = 1 WHERE username = ?",(username,)) - if cur.rowcount == 0: - click.echo("no such user found") - else: - click.echo("ok") - db.commit() + """makes a user an admin user""" + db = get_db() + cur = db.cursor() + cur.execute("UPDATE users SET admin = 1 WHERE username = ?",(username,)) + if cur.rowcount == 0: + click.echo("no such user found") + else: + click.echo("ok") + db.commit() def init_app(app): - app.cli.add_command(make_admin) - app.context_processor(lambda: dict(is_admin=is_admin())) + app.cli.add_command(make_admin) + app.context_processor(lambda: dict(is_admin=is_admin())) diff --git a/apioforum/read.py b/apioforum/read.py index 289b40e..b09a1b2 100644 --- a/apioforum/read.py +++ b/apioforum/read.py @@ -2,11 +2,11 @@ from flask import g from .db import get_db def is_read(type, id): - if g.user == None: - return False - db = get_db() - read = db.execute("SELECT * FROM read WHERE user = ? AND " + type + " = ?;", (g.user, id)).fetchone() - if read == None: - return False - updated = db.execute("SELECT * FROM " + type + "s WHERE id = ?;", (id,)).fetchone()['updated'] - return updated == None or read['time'] >= updated + if g.user == None: + return False + db = get_db() + read = db.execute("SELECT * FROM read WHERE user = ? AND " + type + " = ?;", (g.user, id)).fetchone() + if read == None: + return False + updated = db.execute("SELECT * FROM " + type + "s WHERE id = ?;", (id,)).fetchone()['updated'] + return updated == None or read['time'] >= updated diff --git a/apioforum/roles.py b/apioforum/roles.py index aa1d239..42ce21c 100644 --- a/apioforum/roles.py +++ b/apioforum/roles.py @@ -3,95 +3,95 @@ from .db import get_db from .permissions import is_admin permissions = [ - "p_create_threads", - "p_reply_threads", - "p_manage_threads", - "p_delete_posts", - "p_view_threads", - "p_vote", - "p_create_polls", - "p_approve", - "p_create_subforum", - "p_view_forum" + "p_create_threads", + "p_reply_threads", + "p_manage_threads", + "p_delete_posts", + "p_view_threads", + "p_vote", + "p_create_polls", + "p_approve", + "p_create_subforum", + "p_view_forum" ] def get_role_config(forum_id, role): - db = get_db() + db = get_db() - fid = forum_id - the = None - while the == None and fid != None: - the = db.execute(""" - SELECT * FROM role_config - WHERE forum = ? AND role = ?; - """, (fid,role)).fetchone() - fid = db.execute(""" - SELECT * FROM forums WHERE id = ? - """,(fid,)).fetchone()['parent'] - if the == None: - if role == "other": - raise(RuntimeError( - "unable to find permissions for role 'other', " + - "which should have associated permissions in all contexts.")) - else: - return get_role_config(forum_id, "other") - return the + fid = forum_id + the = None + while the == None and fid != None: + the = db.execute(""" + SELECT * FROM role_config + WHERE forum = ? AND role = ?; + """, (fid,role)).fetchone() + fid = db.execute(""" + SELECT * FROM forums WHERE id = ? + """,(fid,)).fetchone()['parent'] + if the == None: + if role == "other": + raise(RuntimeError( + "unable to find permissions for role 'other', " + + "which should have associated permissions in all contexts.")) + else: + return get_role_config(forum_id, "other") + return the def get_user_role(forum_id, username): - db = get_db() - user = db.execute('SELECT * FROM users WHERE username = ?', - (username,)).fetchone() - if user == None: return "other" - if user['admin']: return "bureaucrat" - - fid = forum_id - the = None - while fid != None: - r = db.execute(""" - SELECT * FROM role_assignments - WHERE forum = ? AND user = ?; - """,(fid,username)).fetchone() - # the user's role is equal to the role assignnment of the closest - # ancestor unless the user's role is "bureaucrat" in any ancestor - # in which case, the users role is "bureaucrat" - if the == None or (r and r['role'] == "bureaucrat"): - the = r - fid = db.execute(""" - SELECT * FROM forums WHERE id = ? - """,(fid,)).fetchone()['parent'] - return the['role'] if the != None else 'other' + db = get_db() + user = db.execute('SELECT * FROM users WHERE username = ?', + (username,)).fetchone() + if user == None: return "other" + if user['admin']: return "bureaucrat" + + fid = forum_id + the = None + while fid != None: + r = db.execute(""" + SELECT * FROM role_assignments + WHERE forum = ? AND user = ?; + """,(fid,username)).fetchone() + # the user's role is equal to the role assignnment of the closest + # ancestor unless the user's role is "bureaucrat" in any ancestor + # in which case, the users role is "bureaucrat" + if the == None or (r and r['role'] == "bureaucrat"): + the = r + fid = db.execute(""" + SELECT * FROM forums WHERE id = ? + """,(fid,)).fetchone()['parent'] + return the['role'] if the != None else 'other' def get_forum_roles(forum_id): - db = get_db() + db = get_db() - ancestors = db.execute(""" - WITH RECURSIVE fs AS - (SELECT * FROM forums WHERE id = ? - UNION ALL - SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id) - SELECT * FROM fs; - """,(forum_id,)).fetchall() - configs = [] - for a in ancestors: - configs += db.execute(""" - SELECT * FROM role_config WHERE forum = ? - """,(a['id'],)).fetchall() - return set(r['role'] for r in configs) + ancestors = db.execute(""" + WITH RECURSIVE fs AS + (SELECT * FROM forums WHERE id = ? + UNION ALL + SELECT forums.* FROM forums, fs WHERE fs.parent=forums.id) + SELECT * FROM fs; + """,(forum_id,)).fetchall() + configs = [] + for a in ancestors: + configs += db.execute(""" + SELECT * FROM role_config WHERE forum = ? + """,(a['id'],)).fetchall() + return set(r['role'] for r in configs) def has_permission(forum_id, username, permission, login_required=True): - db = get_db() - forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum_id,)).fetchone() - user = db.execute('SELECT * FROM users WHERE username = ?', - (username,)).fetchone() if username else None + db = get_db() + forum = db.execute("SELECT * FROM forums WHERE id = ?",(forum_id,)).fetchone() + user = db.execute('SELECT * FROM users WHERE username = ?', + (username,)).fetchone() if username else None - if forum['unlisted'] and not (user and user['admin']): return False - if username == None and login_required: return False + if forum['unlisted'] and not (user and user['admin']): return False + if username == None and login_required: return False - role = get_user_role(forum_id, username) if username else "other" - if role == "bureaucrat": return True - config = get_role_config(forum_id, role) - return config[permission] + role = get_user_role(forum_id, username) if username else "other" + if role == "bureaucrat": return True + config = get_role_config(forum_id, role) + return config[permission] def is_bureaucrat(forum_id, user): - if user == None: return False - return get_user_role(forum_id, user) == "bureaucrat" + if user == None: return False + return get_user_role(forum_id, user) == "bureaucrat" diff --git a/apioforum/static/style.css b/apioforum/static/style.css index c77ec69..63c7347 100644 --- a/apioforum/static/style.css +++ b/apioforum/static/style.css @@ -1,17 +1,17 @@ body { font-family: sans-serif; word-wrap: break-word; } :root { - --alternating-colour-even: hsl(0, 0%, 96%); - --alternating-colour-odd: hsl(0, 0%, 91%); - --dark-colour: black; + --alternating-colour-even: hsl(0, 0%, 96%); + --alternating-colour-odd: hsl(0, 0%, 91%); + --dark-colour: black; --gray: darkgray; - --light-colour: white; - --username-colour: hsl(0, 0%, 25%); - --read-colour: hsl(0, 0%, 30%); - --red: red; - --yellow: yellow; - --blue: blue; - --visited: #552a8b; + --light-colour: white; + --username-colour: hsl(0, 0%, 25%); + --read-colour: hsl(0, 0%, 30%); + --red: red; + --yellow: yellow; + --blue: blue; + --visited: #552a8b; } a { color: var(--blue) } @@ -21,10 +21,10 @@ a:visited { color: var(--visited); } .post:nth-child(even) { background-color: var(--alternating-colour-even) } .post:nth-child(odd) { background-color: var(--alternating-colour-odd) } .post { - border-left: 1px solid var(--dark-colour); - border-right: 1px solid var(--dark-colour); - border-top: 1px solid var(--dark-colour); - width: 100%; + border-left: 1px solid var(--dark-colour); + border-right: 1px solid var(--dark-colour); + border-top: 1px solid var(--dark-colour); + width: 100%; box-sizing: border-box; } .post:last-of-type { border-bottom: 1px solid var(--dark-colour); } @@ -34,8 +34,8 @@ a:visited { color: var(--visited); } color: var(--username-colour); } .username,nav#navbar .username{ - font-weight: bold; - text-decoration: underline; + font-weight: bold; + text-decoration: underline; } .post-heading-em { font-weight: bold; } .post-content * { margin-bottom: 8px; margin-top: 8px; } @@ -52,9 +52,9 @@ a:visited { color: var(--visited); } div.deleted-post { color:var(--light-colour); background-color: var(--dark-colour) !important; - border-left: 1px solid var(--dark-colour); - border-right: 1px solid var(--dark-colour); - border-top: 1px solid var(--dark-colour); + border-left: 1px solid var(--dark-colour); + border-right: 1px solid var(--dark-colour); + border-top: 1px solid var(--dark-colour); } .deleted-post > .post-heading > * { color: hsl(0,0%,85%); @@ -95,22 +95,22 @@ div.deleted-post { } .thread-top-bar, .user-top-bar { - margin-bottom: 4px; + margin-bottom: 4px; } .thread-top-bar-b { - float: right; - margin-right: -2px; + float: right; + margin-right: -2px; } .thread-top-bar-b .tag { - font-size: .9rem; + font-size: .9rem; } .user_info { - border: 1px solid var(--dark-colour); + border: 1px solid var(--dark-colour); background-color: var(--alternating-colour-even); - width: 100%; + width: 100%; padding: 4px; } .user_bio_quote { width: max-content; max-width: 100% } @@ -195,9 +195,9 @@ nav#pages .pageno { align-self: center; } .listing:nth-child(odd) { background-color: var(--alternating-colour-odd) } .listing { - border-left: 1px solid var(--dark-colour); - border-right: 1px solid var(--dark-colour); - border-top: 1px solid var(--dark-colour); + border-left: 1px solid var(--dark-colour); + border-right: 1px solid var(--dark-colour); + border-top: 1px solid var(--dark-colour); padding: 10px; } .listing:last-of-type { border-bottom: 1px solid var(--dark-colour); } @@ -273,12 +273,12 @@ nav#pages .pageno { align-self: center; } } .actionbutton::before { - content: "["; - color: var(--gray); + content: "["; + color: var(--gray); } .actionbutton::after { - content: "]"; - color: var(--gray); + content: "]"; + color: var(--gray); } .actionbutton,.actionbutton:visited { color:var(--blue); @@ -286,32 +286,32 @@ nav#pages .pageno { align-self: center; } } .new-post-box, .forum-desc-box { - height:20em; - resize:vertical; - width:100%; - border:1px solid var(--dark-colour); - margin-top: 5px; + height:20em; + resize:vertical; + width:100%; + border:1px solid var(--dark-colour); + margin-top: 5px; } #polloptions { display: block; resize: vertical; - border:1px solid var(--dark-colour); - margin-top: 5px; + border:1px solid var(--dark-colour); + margin-top: 5px; height: 5em; width: 100%; font-family: sans-serif; } main { - max-width: 80ch; - margin: auto; + max-width: 80ch; + margin: auto; } blockquote { - margin-left: 10px; - padding-left: 10px; - border-left: 3px solid var(--gray); + margin-left: 10px; + padding-left: 10px; + border-left: 3px solid var(--gray); } label { user-select: none; } @@ -321,23 +321,23 @@ fieldset { margin-bottom: 15px; } .warning { color: var(--red); font-weight: bold } .inline-form { - display: inline-block; + display: inline-block; } .tag { - font-size: .75rem; - padding: 1px 3px; - border: 1px solid var(--dark-colour); + font-size: .75rem; + padding: 1px 3px; + border: 1px solid var(--dark-colour); white-space: nowrap; } .md table { - border: 1px solid var(--gray); - border-collapse: collapse; + border: 1px solid var(--gray); + border-collapse: collapse; } .md table td,.md table th { - border: 1px solid var(--gray); - padding: 4px; + border: 1px solid var(--gray); + padding: 4px; } .role-input, .name-input { width: 12ch; } @@ -345,16 +345,16 @@ fieldset { margin-bottom: 15px; } .thing-id { color: var(--gray); font-size: smaller; font-weight: normal; } .breadcrumbs { - list-style: none; + list-style: none; } .breadcrumbs li { - display: inline; + display: inline; } .breadcrumbs li+li::before { - content: "/\00a0"; - padding: 8px; + content: "/\00a0"; + padding: 8px; } textarea { diff --git a/apioforum/templates/admin/admin_page.html b/apioforum/templates/admin/admin_page.html index f48c6c0..fb558bf 100644 --- a/apioforum/templates/admin/admin_page.html +++ b/apioforum/templates/admin/admin_page.html @@ -6,9 +6,9 @@ {% block content %} <h2>admins</h2> <ul> - {% for admin in admins %} - <li>{{admin.username}}</li> - {% endfor %} + {% for admin in admins %} + <li>{{admin.username}}</li> + {% endfor %} </ul> <p>this page will have more things on it later, probably</p> {% endblock %} diff --git a/apioforum/templates/auth/login.html b/apioforum/templates/auth/login.html index 89f490f..361650f 100644 --- a/apioforum/templates/auth/login.html +++ b/apioforum/templates/auth/login.html @@ -1,20 +1,20 @@ {% extends "base.html" %} {% block header %} - <h1>{% block title %}login{% endblock %}</h1> + <h1>{% block title %}login{% endblock %}</h1> {% endblock %} {% block content %} <p>log in using an existing account here. if you don't already have an account, <a href="{{url_for('auth.register')}}">register</a> first instead.</p> <form method="post"> - <label for="username">username</label> - <input name="username" id="username" required> - <br> - <label for="password">password</label> - <input type="password" name="password" id="password" required> - <br> - <input type="checkbox" name="keep_logged_in" id="keep_logged_in"> - <label for="keep_logged_in">keep me logged in</label> - <br> - <input type="submit" value="login"> + <label for="username">username</label> + <input name="username" id="username" required> + <br> + <label for="password">password</label> + <input type="password" name="password" id="password" required> + <br> + <input type="checkbox" name="keep_logged_in" id="keep_logged_in"> + <label for="keep_logged_in">keep me logged in</label> + <br> + <input type="submit" value="login"> </form> {% endblock %} diff --git a/apioforum/templates/auth/register.html b/apioforum/templates/auth/register.html index 082a95b..ed09c56 100644 --- a/apioforum/templates/auth/register.html +++ b/apioforum/templates/auth/register.html @@ -1,20 +1,20 @@ {% extends "base.html" %} {% block header %} - <h1>{% block title %}register{% endblock %}</h1> + <h1>{% block title %}register{% endblock %}</h1> {% endblock %} {% block content %} <p>create a new account here. if you already have an account, <a href="{{url_for('auth.login')}}">login</a> instead.</p> <form method="post"> - <label for="username">username</label> - <input name="username" id="username" maxlength="20" required> - <br> - <label for="password">password</label> - <input type="password" name="password" id="password" required> - <br> - <input type="checkbox" name="keep_logged_in" id="keep_logged_in"> - <label for="keep_logged_in">keep me logged in</label> - <br> - <input type="submit" value="register"> + <label for="username">username</label> + <input name="username" id="username" maxlength="20" required> + <br> + <label for="password">password</label> + <input type="password" name="password" id="password" required> + <br> + <input type="checkbox" name="keep_logged_in" id="keep_logged_in"> + <label for="keep_logged_in">keep me logged in</label> + <br> + <input type="submit" value="register"> </form> {% endblock %} diff --git a/apioforum/templates/base.html b/apioforum/templates/base.html index a202300..ed0a195 100644 --- a/apioforum/templates/base.html +++ b/apioforum/templates/base.html @@ -2,15 +2,15 @@ {% from 'common.html' import disp_user with context %} <!DOCTYPE html> <html> - <head> - <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/i/a.ico"> - </head> - <body> - <nav aria-label="main" id="navbar"> + <head> + <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/i/a.ico"> + </head> + <body> + <nav aria-label="main" id="navbar"> <p style="font-family: monospace;"><b><red>ap</red><orange>i</orange><yellow>o</yellow><green>f</green><blue>o</blue><indigo>r</indigo><violet>um</violet></b>™</p> <form class="inline-form" action="/search"> <input type="search" placeholder="query" name="q"> @@ -27,7 +27,7 @@ {% if is_admin %} <p><a href="{{url_for('admin.admin_page')}}">admin</a></p> {% endif %} - + <p> <a href="{{url_for('auth.logout',next=path_for_next)}}"> logout @@ -47,24 +47,24 @@ {% endif %} </div> - </nav> + </nav> - <div class="header"> - {% block header %}{% endblock %} - </div> + <div class="header"> + {% block header %}{% endblock %} + </div> - {% for msg in get_flashed_messages() %} - <div class="flashmsg">{{ msg }}</div> - {% endfor %} + {% for msg in get_flashed_messages() %} + <div class="flashmsg">{{ msg }}</div> + {% endfor %} - {% block nmcontent %} - <main> - {%block content %}{% endblock %} - </main> - {% endblock %} - <script>/* bees */</script> + {% block nmcontent %} + <main> + {%block content %}{% endblock %} + </main> + {% endblock %} + <script>/* bees */</script> <!-- citrons was here --> <!-- Complete hybridisation of various species of wild duck gene pools could result in the extinction of many indigenous waterfowl. --> - </body> + </body> </html> diff --git a/apioforum/templates/common.html b/apioforum/templates/common.html index fae4b7c..a24272b 100644 --- a/apioforum/templates/common.html +++ b/apioforum/templates/common.html @@ -4,8 +4,8 @@ {% 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"> + <div class="post-heading"> + <span class="post-heading-a"> {% if not post.deleted %} {{disp_user(post.author)}} {% else %} @@ -27,12 +27,12 @@ {{ts(post.created)}} - {% if post.edited %} - (edited {{ts(post.updated)}}) - {% endif %} - </span> - <span class="post-heading-b"> - {% if buttons and not post.deleted %} + {% if post.edited %} + (edited {{ts(post.updated)}}) + {% endif %} + </span> + <span class="post-heading-b"> + {% if buttons and not post.deleted %} {% if post.author == g.user %} <a class="actionbutton" href="{{url_for('thread.edit_post',post_id=post.id)}}">edit</a> @@ -41,25 +41,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="actionbutton" + href="{{url_for('thread.view_post',post_id=post.id)}}">src</a> + {% endif %} + <a class="post-anchor-link" href="{{post_jump(post.id)}}">#{{post.id}}</a> - </span> - </div> - <div class="post-content md"> + </span> + </div> + <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> + {% if footer %} + <div class="post-footer"> + {{ footer }} + </div> + {% endif %} </div> {% endmacro %} @@ -79,7 +79,7 @@ {% if href is not none -%} href="{{href}}" {%- endif -%}> - {{-the_tag.name-}} + {{-the_tag.name-}} </{{el}}> {%- endmacro %} @@ -123,9 +123,9 @@ <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 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 %} diff --git a/apioforum/templates/config_thread.html b/apioforum/templates/config_thread.html index 0795ccc..1debe76 100644 --- a/apioforum/templates/config_thread.html +++ b/apioforum/templates/config_thread.html @@ -33,22 +33,22 @@ {% 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">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"> + <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"> + <input type="submit" value="confirm: delete poll"> </form> {% endif %} {% endif %} diff --git a/apioforum/templates/create_thread.html b/apioforum/templates/create_thread.html index 04b4f42..90d1410 100644 --- a/apioforum/templates/create_thread.html +++ b/apioforum/templates/create_thread.html @@ -5,11 +5,11 @@ {% block content %} <form method="POST"> - <label for="title">thread title</label> - <input name="title" id="title"> - <br> - <label for="content">thread content</label> - <textarea name="content" id="content" class="new-post-box" placeholder="thread content here"></textarea> - <input type="submit" value="create"> + <label for="title">thread title</label> + <input name="title" id="title"> + <br> + <label for="content">thread content</label> + <textarea name="content" id="content" class="new-post-box" placeholder="thread content here"></textarea> + <input type="submit" value="create"> </form> {% endblock %} diff --git a/apioforum/templates/edit_forum.html b/apioforum/templates/edit_forum.html index f165676..3c07d1a 100644 --- a/apioforum/templates/edit_forum.html +++ b/apioforum/templates/edit_forum.html @@ -5,10 +5,10 @@ {% block content %} <form method="POST"> - <label for="name">forum name</label> + <label for="name">forum name</label> <input name="name" id="name" value="{{name}}" placeholder="apioforum" required maxlength="100"/> - <br> - <label for="description">forum description (markdown enabled)</label> + <br> + <label for="description">forum description (markdown enabled)</label> <textarea name="description" id="description" diff --git a/apioforum/templates/search_results.html b/apioforum/templates/search_results.html index fe016ab..a55dc8a 100644 --- a/apioforum/templates/search_results.html +++ b/apioforum/templates/search_results.html @@ -6,23 +6,23 @@ {%block content%} <div class="results"> - {% for result in results %} - {% if display_thread_id[loop.index0] %} - {% if loop.index0 != 0 %} - </div> - {% endif %} - <h3><a href="{{url_for('thread.view_thread', thread_id=result.thread)}}"> - {{result.thread_title}} - </a></h3> - <div class="posts"> - {% endif %} - {{ disp_post(result, False) }} - {% endfor %} + {% for result in results %} + {% if display_thread_id[loop.index0] %} + {% if loop.index0 != 0 %} + </div> + {% endif %} + <h3><a href="{{url_for('thread.view_thread', thread_id=result.thread)}}"> + {{result.thread_title}} + </a></h3> + <div class="posts"> + {% endif %} + {{ disp_post(result, False) }} + {% endfor %} - {% if results|length > 0 %} - </div> - {% else %} - <p>no results were found for '{{query}}'.</p> - {% endif %} + {% if results|length > 0 %} + </div> + {% else %} + <p>no results were found for '{{query}}'.</p> + {% endif %} </div> {% endblock %} diff --git a/apioforum/templates/view_forum.html b/apioforum/templates/view_forum.html index d37a018..7732b21 100644 --- a/apioforum/templates/view_forum.html +++ b/apioforum/templates/view_forum.html @@ -110,8 +110,8 @@ you do not have permission to create threads in this forum {% for the_tag in avail_tags %} <input type="radio" id="tagfilter-{{the_tag.id}}" - name="tagfilter" value="{{the_tag.id}}" - {% if tagfilter_tag.id == the_tag.id %}checked{% endif %}> + name="tagfilter" value="{{the_tag.id}}" + {% if tagfilter_tag.id == the_tag.id %}checked{% endif %}> <label for="tagfilter-{{the_tag.id}}"> {{tag(the_tag)}} </label> diff --git a/apioforum/templates/view_thread.html b/apioforum/templates/view_thread.html index 6e881cb..fa707a6 100644 --- a/apioforum/templates/view_thread.html +++ b/apioforum/templates/view_thread.html @@ -9,93 +9,93 @@ {% 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%} + {%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") %} - <a class="actionbutton" href="{{url_for('thread.config_thread',thread_id=thread.id)}}">configure thread</a> - {% endif %} + <span class="thread-top-bar-a"> + {% if g.user == thread.creator or has_permission(thread.forum, g.user, "p_manage_threads") %} + <a class="actionbutton" href="{{url_for('thread.config_thread',thread_id=thread.id)}}">configure thread</a> + {% endif %} {% if has_permission(thread.forum, g.user, "p_delete_posts") %} - <a class="actionbutton" href="{{url_for('thread.delete_thread',thread_id=thread.id)}}">delete thread</a> + <a class="actionbutton" href="{{url_for('thread.delete_thread',thread_id=thread.id)}}">delete thread</a> {% endif %} - </span> - - <span class="thread-top-bar-b"> - {% for the_tag in tags %} - {{ tag(the_tag) }} - {% endfor %} - </span> + </span> + + <span class="thread-top-bar-b"> + {% for the_tag in tags %} + {{ tag(the_tag) }} + {% endfor %} + </span> </div> {{ pagination_nav(page,max_pageno,'thread.view_thread',thread_id=thread.id) }} <div class="posts"> - {% for post in posts %} - {% if votes[post.id] %} + {% for post in posts %} + {% 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 %} + {% 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 %} + {% 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 %} + {% endif %} + {% endif %} - {% endset %} + {% endset %} - {{ disp_post(post, forum=thread.forum, buttons=True, footer=footer) }} - - {% else %} - {{ disp_post(post, forum=thread.forum, buttons=True) }} - {% endif %} - {% endfor %} + {{ 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 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> + <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> + <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 %} + {% 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"> + {% 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> diff --git a/apioforum/thread.py b/apioforum/thread.py index fdd72e1..9352afb 100644 --- a/apioforum/thread.py +++ b/apioforum/thread.py @@ -3,8 +3,8 @@ import itertools, math from flask import ( - Blueprint, render_template, abort, request, g, redirect, - url_for, flash, jsonify + Blueprint, render_template, abort, request, g, redirect, + url_for, flash, jsonify ) from .db import get_db from .roles import has_permission @@ -17,403 +17,403 @@ bp = Blueprint("thread", __name__, url_prefix="/thread") POSTS_PER_PAGE = 28 def which_page(post_id,return_thread_id=False): - # on which page lieth the post in question? - # forget not that page numbers employeth a system that has a base of 1. - # the - # we need impart the knowledgf e into ourselves pertaining to the - # number of things - # before the thing - # yes - - db = get_db() - # ASSUMES THAT post ids are consecutive and things - # this is probably a reasonable assumption - - thread_id = db.execute('select thread from posts where id = ?',(post_id,)).fetchone()['thread'] - - number_of_things_before_the_thing = db.execute('select count(*) as c, thread as t from posts where thread = ? and id < ?;',(thread_id,post_id)).fetchone()['c'] - - - page = 1+math.floor(number_of_things_before_the_thing/POSTS_PER_PAGE) - if return_thread_id: - return page, thread_id - else: - return page + # on which page lieth the post in question? + # forget not that page numbers employeth a system that has a base of 1. + # the + # we need impart the knowledgf e into ourselves pertaining to the + # number of things + # before the thing + # yes + + db = get_db() + # ASSUMES THAT post ids are consecutive and things + # this is probably a reasonable assumption + + thread_id = db.execute('select thread from posts where id = ?',(post_id,)).fetchone()['thread'] + + number_of_things_before_the_thing = db.execute('select count(*) as c, thread as t from posts where thread = ? and id < ?;',(thread_id,post_id)).fetchone()['c'] + + + page = 1+math.floor(number_of_things_before_the_thing/POSTS_PER_PAGE) + if return_thread_id: + return page, thread_id + else: + return page def post_jump(post_id,*,external=False): - page,thread_id=which_page(post_id,True) - return url_for("thread.view_thread",thread_id=thread_id,page=page,_external=external)+"#post_"+str(post_id) + page,thread_id=which_page(post_id,True) + return url_for("thread.view_thread",thread_id=thread_id,page=page,_external=external)+"#post_"+str(post_id) @bp.route("/<int:thread_id>") @bp.route("/<int:thread_id>/page/<int:page>") def view_thread(thread_id,page=1): - db = get_db() - thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone() - if thread is None: - abort(404) - if not has_permission(thread['forum'], g.user, "p_view_threads", False): - abort(403) - - num_posts = db.execute("SELECT count(*) as count FROM posts WHERE posts.thread = ?",(thread_id,)).fetchone()['count'] - max_pageno = math.ceil(num_posts/POSTS_PER_PAGE) - if page < 1: - abort(404) - elif page > max_pageno and (max_pageno > 0 or page != 1): - abort(404) - - posts = db.execute(""" - SELECT * FROM posts - WHERE posts.thread = ? - ORDER BY created ASC - LIMIT ? OFFSET ?; - """,( - thread_id, - POSTS_PER_PAGE, - (page-1)*POSTS_PER_PAGE, - )).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() - 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 - - if g.user != None: - db.execute("DELETE FROM read WHERE user = ? AND thread = ?;", (g.user, thread_id)) - db.execute("INSERT INTO read (user,thread,time) VALUES (?,?,current_timestamp);", (g.user, thread_id)) - db.commit() - - return render_template( - "view_thread.html", - posts=posts, - thread=thread, - tags=tags, - poll=poll, - votes=votes, - has_voted=has_voted, - page=page, - max_pageno=max_pageno, - ) + db = get_db() + thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone() + if thread is None: + abort(404) + if not has_permission(thread['forum'], g.user, "p_view_threads", False): + abort(403) + + num_posts = db.execute("SELECT count(*) as count FROM posts WHERE posts.thread = ?",(thread_id,)).fetchone()['count'] + max_pageno = math.ceil(num_posts/POSTS_PER_PAGE) + if page < 1: + abort(404) + elif page > max_pageno and (max_pageno > 0 or page != 1): + abort(404) + + posts = db.execute(""" + SELECT * FROM posts + WHERE posts.thread = ? + ORDER BY created ASC + LIMIT ? OFFSET ?; + """,( + thread_id, + POSTS_PER_PAGE, + (page-1)*POSTS_PER_PAGE, + )).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() + 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 + + if g.user != None: + db.execute("DELETE FROM read WHERE user = ? AND thread = ?;", (g.user, thread_id)) + db.execute("INSERT INTO read (user,thread,time) VALUES (?,?,current_timestamp);", (g.user, thread_id)) + db.commit() + + return render_template( + "view_thread.html", + posts=posts, + thread=thread, + tags=tags, + poll=poll, + votes=votes, + has_voted=has_voted, + page=page, + max_pageno=max_pageno, + ) 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 + 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 + 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 - + 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: - flash("you need to log in before you can post") - db = get_db() - content = request.form['content'] - thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone() - if len(content.strip()) == 0: - flash("you cannot post an empty message") - elif not thread: - flash("that thread does not exist") - elif not has_permission(thread['forum'], g.user, "p_reply_threads"): - 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,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 = ?;", - (thread_id,) - ) - db.commit() - forum.set_updated(thread['forum']) - post = db.execute("select * from posts where id = ?",(post_id,)).fetchone() - webhooks.do_webhooks_post(thread['forum'],post) - flash("post posted postfully") - return redirect(post_jump(post_id)) - return redirect(url_for('thread.view_thread',thread_id=thread_id)) + if g.user is None: + flash("you need to log in before you can post") + db = get_db() + content = request.form['content'] + thread = db.execute("SELECT * FROM threads WHERE id = ?;",(thread_id,)).fetchone() + if len(content.strip()) == 0: + flash("you cannot post an empty message") + elif not thread: + flash("that thread does not exist") + elif not has_permission(thread['forum'], g.user, "p_reply_threads"): + 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,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 = ?;", + (thread_id,) + ) + db.commit() + forum.set_updated(thread['forum']) + post = db.execute("select * from posts where id = ?",(post_id,)).fetchone() + webhooks.do_webhooks_post(thread['forum'],post) + flash("post posted postfully") + return redirect(post_jump(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): - db = get_db() - post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone() - thread = db.execute("SELECT * FROM threads WHERE id = ?",(post['thread'],)).fetchone() - if post is None: - flash("that post doesn't exist") - return redirect("/") - if post['author'] != g.user and not has_permission(thread['forum'], g.user, "p_delete_posts"): - flash("you do not have permission to do that") - return redirect(url_for("thread.view_thread",thread_id=post["thread"])) - if request.method == "POST": - db.execute(""" - UPDATE posts SET - content = '', - deleted = 1 - WHERE id = ?""",(post_id,)) - db.commit() - flash("post deleted deletedly") - return redirect(url_for("thread.view_thread",thread_id=post["thread"])) - else: - return render_template("delete_post.html",post=post) - + db = get_db() + post = db.execute("SELECT * FROM posts WHERE id = ?",(post_id,)).fetchone() + thread = db.execute("SELECT * FROM threads WHERE id = ?",(post['thread'],)).fetchone() + if post is None: + flash("that post doesn't exist") + return redirect("/") + if post['author'] != g.user and not has_permission(thread['forum'], g.user, "p_delete_posts"): + flash("you do not have permission to do that") + return redirect(url_for("thread.view_thread",thread_id=post["thread"])) + if request.method == "POST": + db.execute(""" + UPDATE posts SET + content = '', + deleted = 1 + WHERE id = ?""",(post_id,)) + db.commit() + flash("post deleted deletedly") + return redirect(url_for("thread.view_thread",thread_id=post["thread"])) + else: + return render_template("delete_post.html",post=post) + @bp.route("/delete_thread/<int:thread_id>", methods=["GET","POST"]) def delete_thread(thread_id): - db = get_db() - thread = db.execute("SELECT * FROM threads WHERE id = ?",(thread_id,)).fetchone() - if thread is None: - flash("that thread doesn't exist") - return redirect("/") - if not has_permission(thread['forum'], g.user, "p_delete_posts"): - flash("you do not have permission to do that") - return redirect(url_for("thread.view_thread",thread_id=thread_id)) - if request.method == "POST": - db.execute("DELETE FROM posts WHERE thread = ?",(thread_id,)) - db.execute("DELETE FROM threads WHERE id = ?",(thread_id,)) - db.commit() - flash("thread deleted deletedly") - return redirect(url_for("forum.view_forum",forum_id=thread['forum'])) - else: - count = db.execute("SELECT num_replies FROM number_of_posts WHERE thread = ?", - (thread_id,)).fetchone()[0] - return render_template("delete_thread.html",thread=thread,post_count=count) - + db = get_db() + thread = db.execute("SELECT * FROM threads WHERE id = ?",(thread_id,)).fetchone() + if thread is None: + flash("that thread doesn't exist") + return redirect("/") + if not has_permission(thread['forum'], g.user, "p_delete_posts"): + flash("you do not have permission to do that") + return redirect(url_for("thread.view_thread",thread_id=thread_id)) + if request.method == "POST": + db.execute("DELETE FROM posts WHERE thread = ?",(thread_id,)) + db.execute("DELETE FROM threads WHERE id = ?",(thread_id,)) + db.commit() + flash("thread deleted deletedly") + return redirect(url_for("forum.view_forum",forum_id=thread['forum'])) + else: + count = db.execute("SELECT num_replies FROM number_of_posts WHERE thread = ?", + (thread_id,)).fetchone()[0] + return render_template("delete_thread.html",thread=thread,post_count=count) + @bp.route("/edit_post/<int:post_id>",methods=["GET","POST"]) def edit_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')) - - if post['author'] != g.user: - flash("you can only edit posts that you created") - return redirect(url_for("thread.view_thread",thread_id=post['thread'])) - # note: i am writing this while i am very tired, so probably - # come back and test this properly later - if request.method == "POST": - err = None - newcontent = request.form['newcontent'] - if len(newcontent.strip()) == 0: - err="post contents can't be empty" - print(err) - if err is None: - db.execute( - "UPDATE posts SET content = ?, edited = 1, updated = current_timestamp WHERE id = ?",(newcontent,post_id)) - db.commit() - flash("post edited editiously") - return redirect(post_jump(post_id)) - else: - flash(err) - return render_template("edit_post.html",post=post) + 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')) + + if post['author'] != g.user: + flash("you can only edit posts that you created") + return redirect(url_for("thread.view_thread",thread_id=post['thread'])) + # note: i am writing this while i am very tired, so probably + # come back and test this properly later + if request.method == "POST": + err = None + newcontent = request.form['newcontent'] + if len(newcontent.strip()) == 0: + err="post contents can't be empty" + print(err) + if err is None: + db.execute( + "UPDATE posts SET content = ?, edited = 1, updated = current_timestamp WHERE id = ?",(newcontent,post_id)) + db.commit() + flash("post edited editiously") + return redirect(post_jump(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 p.*,h.f_id FROM posts p - INNER JOIN forum_thread_of_post h ON p.id = h.p_id - WHERE p.id = ?; - """,(post_id,)).fetchone() - - if post is None: - abort(404) - - if not has_permission(post['f_id'], g.user, "p_view_threads", False): - abort(403) - - return render_template("view_post.html",post=post) - - - + db = get_db() + post = db.execute(""" + SELECT p.*,h.f_id FROM posts p + INNER JOIN forum_thread_of_post h ON p.id = h.p_id + WHERE p.id = ?; + """,(post_id,)).fetchone() + + if post is None: + abort(404) + + if not has_permission(post['f_id'], g.user, "p_view_threads", False): + abort(403) + + 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 = get_avail_tags(thread['forum']) - err = None - if g.user is None: - err = "you need to be logged in to do that" - elif not has_permission(thread['forum'], g.user, "p_view_threads"): - err = "you do not have permission to do that" - elif g.user != thread['creator'] and not has_permission(thread['forum'], g.user, "p_manage_threads"): - err = "you can only configure threads that you own" - - if err is not None: - flash(err) - return redirect(url_for("thread.view_thread",thread_id=thread_id)) - - if request.method == "POST": - err = [] - if request.form['title'] != thread['title']: - title = request.form['title'] - if len(title.strip()) == 0: - err.append("title can't be empty") - else: - db.execute("update threads set title = ? where id = ?;",(title,thread_id)) - flash("title updated successfully") - db.commit() - changed = False - 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: - db.execute("insert into thread_tags (thread, tag) values (?,?)",(thread_id,tagid)) - changed = True - elif current and not wanted: - db.execute("delete from thread_tags where thread = ? and tag = ?",(thread_id,tagid)) - changed = True - if changed: - db.commit() - flash("tags updated successfully") - - if len(err) > 0: - for e in err: - flash(e) - else: - return redirect(url_for("thread.view_thread",thread_id=thread_id)) - - - return render_template("config_thread.html", thread=thread,thread_tags=thread_tags,avail_tags=avail_tags) - + 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 = get_avail_tags(thread['forum']) + err = None + if g.user is None: + err = "you need to be logged in to do that" + elif not has_permission(thread['forum'], g.user, "p_view_threads"): + err = "you do not have permission to do that" + elif g.user != thread['creator'] and not has_permission(thread['forum'], g.user, "p_manage_threads"): + err = "you can only configure threads that you own" + + if err is not None: + flash(err) + return redirect(url_for("thread.view_thread",thread_id=thread_id)) + + if request.method == "POST": + err = [] + if request.form['title'] != thread['title']: + title = request.form['title'] + if len(title.strip()) == 0: + err.append("title can't be empty") + else: + db.execute("update threads set title = ? where id = ?;",(title,thread_id)) + flash("title updated successfully") + db.commit() + changed = False + 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: + db.execute("insert into thread_tags (thread, tag) values (?,?)",(thread_id,tagid)) + changed = True + elif current and not wanted: + db.execute("delete from thread_tags where thread = ? and tag = ?",(thread_id,tagid)) + changed = True + if changed: + db.commit() + flash("tags updated successfully") + + if len(err) > 0: + for e in err: + flash(e) + else: + return redirect(url_for("thread.view_thread",thread_id=thread_id)) + + + return render_template("config_thread.html", thread=thread,thread_tags=thread_tags,avail_tags=avail_tags) + diff --git a/apioforum/user.py b/apioforum/user.py index 8fb59ab..cead2fe 100644 --- a/apioforum/user.py +++ b/apioforum/user.py @@ -2,7 +2,7 @@ POSTS_PER_PAGE = 28 from flask import ( - Blueprint, render_template, abort, g, flash, redirect, url_for, request + Blueprint, render_template, abort, g, flash, redirect, url_for, request ) from werkzeug.security import check_password_hash, generate_password_hash @@ -10,72 +10,72 @@ from .db import get_db import math bp = Blueprint("user", __name__, url_prefix="/user") - + @bp.route("/<username>") @bp.route("/<username>/page/<int:page>") def view_user(username, page=1): - if page < 1: - abort(400) - - db = get_db() - user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone() + if page < 1: + abort(400) + + db = get_db() + user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone() - if user is None: - abort(404) - posts = db.execute(""" - SELECT * FROM posts - JOIN public_posts ON public_posts.id = posts.id - WHERE author = ? AND deleted = 0 AND public_posts.public - ORDER BY created DESC - LIMIT ? OFFSET ?;""",(username,POSTS_PER_PAGE,(page-1)*POSTS_PER_PAGE,)).fetchall() - num_posts = db.execute(""" - SELECT count(*) as count FROM posts - JOIN public_posts ON public_posts.id = posts.id - WHERE author = ? AND public_posts.public; - """,(username,)).fetchone()['count'] - max_pageno = math.ceil(num_posts/POSTS_PER_PAGE) - return render_template( - "view_user.html", - user=user, - posts=posts, - page=page, - max_pageno=max_pageno, - ) + if user is None: + abort(404) + posts = db.execute(""" + SELECT * FROM posts + JOIN public_posts ON public_posts.id = posts.id + WHERE author = ? AND deleted = 0 AND public_posts.public + ORDER BY created DESC + LIMIT ? OFFSET ?;""",(username,POSTS_PER_PAGE,(page-1)*POSTS_PER_PAGE,)).fetchall() + num_posts = db.execute(""" + SELECT count(*) as count FROM posts + JOIN public_posts ON public_posts.id = posts.id + WHERE author = ? AND public_posts.public; + """,(username,)).fetchone()['count'] + max_pageno = math.ceil(num_posts/POSTS_PER_PAGE) + return render_template( + "view_user.html", + user=user, + posts=posts, + page=page, + max_pageno=max_pageno, + ) @bp.route("/<username>/edit", methods=["GET","POST"]) def edit_user(username): - db = get_db() - user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone() - if user is None: - abort(404) - if username != g.user: - flash("you cannot modify other people") - return redirect(url_for("user.view_user",username=username)) + db = get_db() + user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone() + if user is None: + abort(404) + if username != g.user: + flash("you cannot modify other people") + return redirect(url_for("user.view_user",username=username)) - if request.method == "POST": - err = [] - if len(request.form['new_password']) > 0: - if not check_password_hash(user['password'],request.form['password']): - err.append("entered password does not match current password") - else: - db.execute("update users set password = ? where username = ?", - (generate_password_hash(request.form["new_password"]), username)) - db.commit() - flash("password changed changefully") - if request.form['bio'] != user['bio']: - if len(request.form['bio'].strip()) == 0: - err.append("please submit nonempty bio") - elif len(request.form['bio']) > 4500: - err.append("bio is too long!!") - else: - db.execute("update users set bio = ? where username = ?", (request.form['bio'], username)) - db.commit() - flash("bio updated successfully") + if request.method == "POST": + err = [] + if len(request.form['new_password']) > 0: + if not check_password_hash(user['password'],request.form['password']): + err.append("entered password does not match current password") + else: + db.execute("update users set password = ? where username = ?", + (generate_password_hash(request.form["new_password"]), username)) + db.commit() + flash("password changed changefully") + if request.form['bio'] != user['bio']: + if len(request.form['bio'].strip()) == 0: + err.append("please submit nonempty bio") + elif len(request.form['bio']) > 4500: + err.append("bio is too long!!") + else: + db.execute("update users set bio = ? where username = ?", (request.form['bio'], username)) + db.commit() + flash("bio updated successfully") - if len(err) > 0: - for e in err: - flash(e) - else: - return redirect(url_for("user.view_user",username=username)) - - return render_template("user_settings.html",user=user) + if len(err) > 0: + for e in err: + flash(e) + else: + return redirect(url_for("user.view_user",username=username)) + + return render_template("user_settings.html",user=user) diff --git a/apioforum/util.py b/apioforum/util.py index 64bdf20..8f836d1 100644 --- a/apioforum/util.py +++ b/apioforum/util.py @@ -5,12 +5,12 @@ 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 - - + b=s.encode("utf-8") + h=hashlib.sha1(b) + two_bytes=h.digest()[:2] + val = int.from_bytes(two_bytes, 'little') + angle = 360*(val/65536) + col = hsluv.hsluv_to_hex([angle, 80, 70]) + return col + + diff --git a/apioforum/webhooks.py b/apioforum/webhooks.py index f7387be..a09f951 100644 --- a/apioforum/webhooks.py +++ b/apioforum/webhooks.py @@ -6,211 +6,211 @@ from flask import url_for, flash def abridge(text,maxlen=20): - if len(text) > maxlen+3: - return text[:maxlen]+"..." - else: - return text + if len(text) > maxlen+3: + return text[:maxlen]+"..." + else: + return text webhook_types = {} def webhook_type(t): - def inner(cls): - webhook_types[t] = cls - return cls - return inner + def inner(cls): + webhook_types[t] = cls + return cls + return inner class WebhookType(abc.ABC): - def __init__(self, url, wh_id): - self.url = url - self.wh_id = wh_id + def __init__(self, url, wh_id): + self.url = url + self.wh_id = wh_id - @abc.abstractmethod - def on_new_thread(self,thread): - pass - @abc.abstractmethod - def on_new_post(self,post): - pass + @abc.abstractmethod + def on_new_thread(self,thread): + pass + @abc.abstractmethod + def on_new_post(self,post): + pass def get_webhooks(forum_id): - db = get_db() - # todo inheritance (if needed) - webhooks = db.execute("select * from webhooks where webhooks.forum = ?;",(forum_id,)).fetchall() - - for wh in webhooks: - wh_type = wh['type'] - if wh_type not in webhook_types: - print(f"unknown webhook type {wh_type}") - continue - wh_url = wh['url'] - wo = webhook_types[wh_type](wh_url, wh['id']) - yield wo + db = get_db() + # todo inheritance (if needed) + webhooks = db.execute("select * from webhooks where webhooks.forum = ?;",(forum_id,)).fetchall() + + for wh in webhooks: + wh_type = wh['type'] + if wh_type not in webhook_types: + print(f"unknown webhook type {wh_type}") + continue + wh_url = wh['url'] + wo = webhook_types[wh_type](wh_url, wh['id']) + yield wo def do_webhooks_thread(forum_id,thread): - for wh in get_webhooks(forum_id): - try: - wh.on_new_thread(thread) - except Exception as e: - #raise e - flash(f"error executing webhook with id {wh.wh_id}") + for wh in get_webhooks(forum_id): + try: + wh.on_new_thread(thread) + except Exception as e: + #raise e + flash(f"error executing webhook with id {wh.wh_id}") def do_webhooks_post(forum_id,post): - for wh in get_webhooks(forum_id): - try: - wh.on_new_post(post) - except Exception as e: - #raise e - flash(f"error executing webhook with id {wh.wh_id}") + for wh in get_webhooks(forum_id): + try: + wh.on_new_post(post) + except Exception as e: + #raise e + flash(f"error executing webhook with id {wh.wh_id}") @webhook_type("fake") class FakeWebhook(WebhookType): - def on_new_post(self, post): - print(f'fake wh {self.url} post {post["id"]}') - def on_new_thread(self, thread): - print(f'fake wh {self.url} thread {thread["id"]}') + def on_new_post(self, post): + print(f'fake wh {self.url} post {post["id"]}') + def on_new_thread(self, thread): + print(f'fake wh {self.url} thread {thread["id"]}') @webhook_type("discord") class DiscordWebhook(WebhookType): - def send(self,payload): - headers = { - "User-Agent":"apioforum (https://g.gh0.pw/apioforum, v0.0)", - "Content-Type":"application/json", - } - req = urllib.request.Request( - self.url, - json.dumps(payload).encode("utf-8"), - headers - ) - # todo: read response and things - urllib.request.urlopen(req) - #try: - # res = urllib.request.urlopen(req) - #except urllib.error.HTTPError as e: - # print(f"error {e.code} {e.read()}") - #else: - # print(f"succ {res.read()}") - - @staticmethod - def field(name,value): - return {"name":name,"value":value,"inline":True} - - def on_new_thread(self,thread): - f = self.field - db = get_db() - forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() - username = thread['creator'] - userpage = url_for('user.view_user',username=username,_external=True) - - forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True) - - post = db.execute("select * from posts where thread = ? order by id asc limit 1",(thread['id'],)).fetchone() - - payload = { - "username":"apioforum", - "avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png", - "embeds":[ - { - "title":"new thread: "+thread['title'], - "description":abridge(post['content']), - "url": url_for('thread.view_thread',thread_id=thread['id'],_external=True), - "color": 0xff00ff, - "fields":[ - f('author',f"[{username}]({userpage})"), - f('forum',f"[{forum['name']}]({forumpage})"), - ], - "footer":{ - "text":thread['created'].isoformat(' '), - }, - }, - ], - } - self.send(payload) - - def on_new_post(self,post): - from .thread import post_jump - f = self.field - db = get_db() - - thread = db.execute("select * from threads where id = ?",(post['thread'],)).fetchone() - threadpage = url_for('thread.view_thread',thread_id=thread['id'],_external=True) - - forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() - forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True) - - username = post['author'] - userpage = url_for('user.view_user',username=username,_external=True) - - payload = { - "username":"apioforum", - "avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png", - "embeds":[ - { - "title":"re: "+thread['title'], - "description":abridge(post['content']), - "url": post_jump(post['id'],external=True), - "color": 0x00ffff, - "fields":[ - f('author',f"[{username}]({userpage})"), - f('thread',f"[{thread['title']}]({threadpage})"), - f('forum',f"[{forum['name']}]({forumpage})"), - ], - "footer":{ - "text":post['created'].isoformat(' '), - }, - }, - ], - } - self.send(payload) + def send(self,payload): + headers = { + "User-Agent":"apioforum (https://g.gh0.pw/apioforum, v0.0)", + "Content-Type":"application/json", + } + req = urllib.request.Request( + self.url, + json.dumps(payload).encode("utf-8"), + headers + ) + # todo: read response and things + urllib.request.urlopen(req) + #try: + # res = urllib.request.urlopen(req) + #except urllib.error.HTTPError as e: + # print(f"error {e.code} {e.read()}") + #else: + # print(f"succ {res.read()}") + + @staticmethod + def field(name,value): + return {"name":name,"value":value,"inline":True} + + def on_new_thread(self,thread): + f = self.field + db = get_db() + forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() + username = thread['creator'] + userpage = url_for('user.view_user',username=username,_external=True) + + forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True) + + post = db.execute("select * from posts where thread = ? order by id asc limit 1",(thread['id'],)).fetchone() + + payload = { + "username":"apioforum", + "avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png", + "embeds":[ + { + "title":"new thread: "+thread['title'], + "description":abridge(post['content']), + "url": url_for('thread.view_thread',thread_id=thread['id'],_external=True), + "color": 0xff00ff, + "fields":[ + f('author',f"[{username}]({userpage})"), + f('forum',f"[{forum['name']}]({forumpage})"), + ], + "footer":{ + "text":thread['created'].isoformat(' '), + }, + }, + ], + } + self.send(payload) + + def on_new_post(self,post): + from .thread import post_jump + f = self.field + db = get_db() + + thread = db.execute("select * from threads where id = ?",(post['thread'],)).fetchone() + threadpage = url_for('thread.view_thread',thread_id=thread['id'],_external=True) + + forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() + forumpage = url_for('forum.view_forum',forum_id=forum['id'],_external=True) + + username = post['author'] + userpage = url_for('user.view_user',username=username,_external=True) + + payload = { + "username":"apioforum", + "avatar_url":"https://d.gh0.pw/lib/exe/fetch.php?media=wiki:logo.png", + "embeds":[ + { + "title":"re: "+thread['title'], + "description":abridge(post['content']), + "url": post_jump(post['id'],external=True), + "color": 0x00ffff, + "fields":[ + f('author',f"[{username}]({userpage})"), + f('thread',f"[{thread['title']}]({threadpage})"), + f('forum',f"[{forum['name']}]({forumpage})"), + ], + "footer":{ + "text":post['created'].isoformat(' '), + }, + }, + ], + } + self.send(payload) @webhook_type("apionet") class ApionetWebhook(WebhookType): - MAXMSGLEN = 420 + MAXMSGLEN = 420 - # using 'url' as path of socket to send to. - def send(self,payload): - import socket - s = socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM) - s.sendto(payload.encode("utf-8"),self.url) - s.close() + # using 'url' as path of socket to send to. + def send(self,payload): + import socket + s = socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM) + s.sendto(payload.encode("utf-8"),self.url) + s.close() - def append_url(self,rest,url): - ub = url.encode("utf-8") - max_rlen = self.MAXMSGLEN - len(ub) - 1 - - while len(rest.encode("utf-8")) > max_rlen: - # chop off characters until short enough - rest = rest[:-1] + def append_url(self,rest,url): + ub = url.encode("utf-8") + max_rlen = self.MAXMSGLEN - len(ub) - 1 + + while len(rest.encode("utf-8")) > max_rlen: + # chop off characters until short enough + rest = rest[:-1] - return rest + " " + url + return rest + " " + url - def on_new_thread(self,thread): - # copy paste from above. the great refactor will fix this all - db = get_db() - forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() - username = thread['creator'][:30] - post = db.execute("select * from posts where thread = ? order by id asc limit 1",(thread['id'],)).fetchone() - url = url_for('thread.view_thread',thread_id=thread['id'],_external=True) + def on_new_thread(self,thread): + # copy paste from above. the great refactor will fix this all + db = get_db() + forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() + username = thread['creator'][:30] + post = db.execute("select * from posts where thread = ? order by id asc limit 1",(thread['id'],)).fetchone() + url = url_for('thread.view_thread',thread_id=thread['id'],_external=True) - p = "" - if forum['id'] != 1: - p = f" in {forum['name']}" - payload = f"new thread{p}: [{abridge(thread['title'],50)}] {username}: {abridge(post['content'],75)}" - self.send(self.append_url(payload,"("+url+")")) + p = "" + if forum['id'] != 1: + p = f" in {forum['name']}" + payload = f"new thread{p}: [{abridge(thread['title'],50)}] {username}: {abridge(post['content'],75)}" + self.send(self.append_url(payload,"("+url+")")) - def on_new_post(self,post): - from .thread import post_jump - db = get_db() + def on_new_post(self,post): + from .thread import post_jump + db = get_db() - thread = db.execute("select * from threads where id = ?",(post['thread'],)).fetchone() - forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() - username = post['author'][:30] + thread = db.execute("select * from threads where id = ?",(post['thread'],)).fetchone() + forum = db.execute("select * from forums where id = ?",(thread['forum'],)).fetchone() + username = post['author'][:30] - url = post_jump(post['id'],external=True) + url = post_jump(post['id'],external=True) - payload = f"[{abridge(thread['title'],50)}] {username}: {abridge(post['content'],75)}" - self.send(self.append_url(payload,"("+url+")")) + payload = f"[{abridge(thread['title'],50)}] {username}: {abridge(post['content'],75)}" + self.send(self.append_url(payload,"("+url+")")) |