diff options
| author | citrons <citrons> | 2021-08-18 04:06:02 +0000 | 
|---|---|---|
| committer | citrons <citrons> | 2021-08-18 04:06:02 +0000 | 
| commit | c49955a06909d2dd081bc82131fe8e1450055ee0 (patch) | |
| tree | c5fffa2cadff7667ec630849c2079de2fc762b9c | |
| parent | 17357a3fb5cf603ff79daad644f4a4c0fbe42150 (diff) | |
begin db refactor
| -rw-r--r-- | apioforum/__init__.py | 2 | ||||
| -rw-r--r-- | apioforum/db.py | 323 | ||||
| -rw-r--r-- | apioforum/db_migrations.py | 190 | ||||
| -rw-r--r-- | apioforum/forum.py | 6 | ||||
| -rw-r--r-- | apioforum/templates/view_thread.html | 18 | ||||
| -rw-r--r-- | apioforum/thread.py | 110 | ||||
| -rw-r--r-- | apioforum/user.py | 32 | 
7 files changed, 428 insertions, 253 deletions
diff --git a/apioforum/__init__.py b/apioforum/__init__.py index 087df81..8300948 100644 --- a/apioforum/__init__.py +++ b/apioforum/__init__.py @@ -22,6 +22,8 @@ def create_app():      from . import db      db.init_app(app) +    app.url_map.converters['db'] = db.DbConverter +      from . import permissions      permissions.init_app(app) diff --git a/apioforum/db.py b/apioforum/db.py index c0c8c7e..e151cd4 100644 --- a/apioforum/db.py +++ b/apioforum/db.py @@ -1,7 +1,10 @@  import sqlite3  import click -from flask import current_app, g +from flask import current_app, g, abort  from flask.cli import with_appcontext +from werkzeug.routing import BaseConverter + +from db_migrations import migrations  def get_db():      if 'db' not in g: @@ -18,197 +21,6 @@ def close_db(e=None):      if db is not None:          db.close() -migrations = [ -""" -CREATE TABLE users ( -    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 -); -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 -); - -CREATE INDEX posts_thread_idx ON posts (thread); -""", -""" -ALTER TABLE posts ADD COLUMN edited INT NOT NULL DEFAULT 0; -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' -); -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); -END; -CREATE TRIGGER posts_ad AFTER DELETE ON posts BEGIN -    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); -END; -""", -""" -CREATE TABLE tags ( -    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) -); -""", -"""CREATE INDEX thread_tags_thread ON thread_tags (thread);""", -"""ALTER TABLE users ADD COLUMN admin INT NOT NULL DEFAULT 0""", -""" -ALTER TABLE users ADD COLUMN bio TEXT; -ALTER TABLE users ADD COLUMN joined TIMESTAMP; -""", -""" -CREATE TABLE polls ( -    id INTEGER PRIMARY KEY, -    title TEXT NOT NULL -); -ALTER TABLE threads ADD COLUMN poll INTEGER REFERENCES polls(id); - -CREATE TABLE poll_options ( -    poll INTEGER NOT NULL REFERENCES polls(id), -    text TEXT NOT NULL, -    option_idx INTEGER NOT NULL, -    PRIMARY KEY ( poll, option_idx ) -); - -CREATE TABLE votes ( -    id INTEGER PRIMARY KEY, -    user TEXT NOT NULL REFERENCES users(username), -    poll INTEGER NOT NULL, -    option_idx INTEGER, -    time TIMESTAMP NOT NULL, -    current INTEGER NOT NULL, -    is_retraction INTEGER, -    CHECK (is_retraction OR (option_idx NOT NULL)), -    FOREIGN KEY ( poll, option_idx ) REFERENCES poll_options(poll, option_idx) -); -ALTER TABLE posts ADD COLUMN vote INTEGER REFERENCES votes(id); -""", -""" -CREATE VIEW vote_counts AS -    SELECT poll, option_idx, count(*) AS num FROM votes WHERE current GROUP BY option_idx,poll;  -""", -""" -CREATE TABLE forums ( -    id INTEGER PRIMARY KEY, -    name TEXT NOT NULL, -    parent INTEGER REFERENCES forums(id), -    description TEXT -); -INSERT INTO forums (name,parent,description) values ('apioforum',null, -        '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) -); -INSERT INTO threads_new (id,title,creator,created,updated,forum) -    SELECT id,title,creator,created,updated,1 FROM threads; -DROP TABLE threads; -ALTER TABLE threads_new RENAME TO threads; -COMMIT; -PRAGMA foreign_keys = on; -""", -""" -CREATE VIEW most_recent_posts AS -    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; -""", -""" -CREATE VIEW total_vote_counts AS -    SELECT poll, count(*) AS total_votes FROM votes WHERE current AND NOT is_retraction GROUP BY poll; -""", -""" -PRAGMA foreign_keys = off; -BEGIN TRANSACTION; -CREATE TABLE tags_new ( -	id INTEGER PRIMARY KEY, -	name TEXT NOT NULL, -	text_colour TEXT NOT NULL, -	bg_colour TEXT NOT NULL, -	forum INTEGER NOT NULL REFERENCES forums(id) -); -INSERT INTO tags_new (id,name,text_colour,bg_colour,forum) -	SELECT id,name,text_colour,bg_colour,1 FROM tags; -DROP TABLE tags; -ALTER TABLE tags_new RENAME TO tags; -PRAGMA foreign_keys = on; -""", -""" -CREATE TABLE role_config ( -    role TEXT NOT NULL, -    forum NOT NULL REFERENCES forums(id), -    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 -); - -INSERT INTO role_config (role,forum) VALUES ("approved",1); -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 -); -""", -""" -ALTER TABLE posts ADD COLUMN deleted NOT NULL DEFAULT 0; -""", -""" -ALTER TABLE forums ADD COLUMN unlisted NOT NULL DEFAULT 0; -""", -""" -ALTER TABLE role_config ADD COLUMN p_view_forum INT NOT NULL DEFAULT 1; -""" -] -  def init_db():      db = get_db()      version = db.execute("PRAGMA user_version;").fetchone()[0] @@ -229,3 +41,130 @@ def init_app(app):      app.teardown_appcontext(close_db)      app.cli.add_command(migrate_command) + +class DbWrapper: +    table = None +    primary_key = "id" +     +    # column name -> DbWrapper child class +    # this allows the DbWrapper to automatically fetch the referenced object +    references = {} + +    @classmethod +    def get_row(cls, key): +        return get_db().execute( +            f"SELECT * FROM {cls.table} WHERE {cls.primary_key} = ?", (key,))\ +                .fetchone() + +    @classmethod +    def fetch(cls, key): +        row = cls.get_row(key) +        if row == None: raise KeyError(key) +        return cls(row) + +    @classmethod +    def query_some(cls, *args, **kwargs): +        rows = get_db().execute(*args, **kwargs).fetchall() +        for row in rows: +            yield cls(row) + +    @classmethod +    def query(cls, *args, **kwargs): +        return(next(cls.query_some(*args, **kwargs))) + +    def __init__(self, row): +        self._row = row +        if self.__class__.primary_key: +            self._key = row[self.__class__.primary_key] +        else: +            self._key = None + +    def __getattr__(self, attr): +        # special attributes are retrieved from the object itself +        if attr[0] == '_': +            if not attr in self.__dict__: +                raise AttributeError() +            return self.__dict__[attr] + +        # changes have been made to the row. fetch it again +        if self._row == None and self._key != None: +            self._row = self.__class__.get_row(self._key) + +        # if this column is a reference, fetch the referenced row as an object +        r = self.__class__.references.get(attr, None) +        if r != None: +            # do not fetch it more than once +            if not attr in self.__dict__: +                self.__dict__[attr] = r.fetch(self._row[attr]) +            return self.__dict__[attr] + +        try: +            return self._row[attr] +        except KeyError as k: +            raise AttributeError() from k + +    def __setattr__(self, attr, value): +        if not self.__class__.primary_key: +            raise(RuntimeError('cannot set attributes on this object')) + +        # special attributes are set on the object itself +        if attr[0] == '_': +            self.__dict__[attr] = value +            return + +        cls = self.__class__ + +        if not isinstance(value, DbWrapper): +            v = value +        else: +            v = value._key + +        print(f"UPDATE {cls.table} SET {attr} = ? WHERE {cls.primary_key} = ?") + +        get_db().execute( +            f"UPDATE {cls.table} SET {attr} = ? WHERE {cls.primary_key} = ?", +                (v, self._key)) + +        # the fetched row is invalidated. +        # avoid extra queries by querying again only if attributes are accessed +        self._row = None + +    def __eq__(self, other): +        if self.__class__.primary_key: +            if isinstance(other, self.__class__): +                # rows with keys are equivalent if their keys are +                return self.__class__.table == other.__class__.table\ +                        and self._key == other._key +            else: +                # a row can be compared with its key +                return self._key == other +        else: +            return self._row == other._row + +    def __conform__(self, protocol): +        # if used in a database query, convert to database key +        if protocol is sqlite3.PrepareProtocol: +            return self._key + +# flask path converter +class DbConverter(BaseConverter): +    def __init__(self, m, db_class, abort=True): +        super(DbConverter, self).__init__(m) +        self.db_class = db_class +        self.abort = abort +     +    def to_python(self, value): +        try: +            return self.db_class.fetch(value) +        except KeyError: +            if self.abort: +                abort(404) +            else: +                return None + +    def to_url(self, value): +        if isinstance(value, self.db_class): +            return str(value._key) +        else: +            return str(value) + diff --git a/apioforum/db_migrations.py b/apioforum/db_migrations.py new file mode 100644 index 0000000..fc6e22a --- /dev/null +++ b/apioforum/db_migrations.py @@ -0,0 +1,190 @@ +migrations = [ +""" +CREATE TABLE users ( +    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 +); +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 +); + +CREATE INDEX posts_thread_idx ON posts (thread); +""", +""" +ALTER TABLE posts ADD COLUMN edited INT NOT NULL DEFAULT 0; +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' +); +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); +END; +CREATE TRIGGER posts_ad AFTER DELETE ON posts BEGIN +    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); +END; +""", +""" +CREATE TABLE tags ( +    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) +); +""", +"""CREATE INDEX thread_tags_thread ON thread_tags (thread);""", +"""ALTER TABLE users ADD COLUMN admin INT NOT NULL DEFAULT 0""", +""" +ALTER TABLE users ADD COLUMN bio TEXT; +ALTER TABLE users ADD COLUMN joined TIMESTAMP; +""", +""" +CREATE TABLE polls ( +    id INTEGER PRIMARY KEY, +    title TEXT NOT NULL +); +ALTER TABLE threads ADD COLUMN poll INTEGER REFERENCES polls(id); + +CREATE TABLE poll_options ( +    poll INTEGER NOT NULL REFERENCES polls(id), +    text TEXT NOT NULL, +    option_idx INTEGER NOT NULL, +    PRIMARY KEY ( poll, option_idx ) +); + +CREATE TABLE votes ( +    id INTEGER PRIMARY KEY, +    user TEXT NOT NULL REFERENCES users(username), +    poll INTEGER NOT NULL, +    option_idx INTEGER, +    time TIMESTAMP NOT NULL, +    current INTEGER NOT NULL, +    is_retraction INTEGER, +    CHECK (is_retraction OR (option_idx NOT NULL)), +    FOREIGN KEY ( poll, option_idx ) REFERENCES poll_options(poll, option_idx) +); +ALTER TABLE posts ADD COLUMN vote INTEGER REFERENCES votes(id); +""", +""" +CREATE VIEW vote_counts AS +    SELECT poll, option_idx, count(*) AS num FROM votes WHERE current GROUP BY option_idx,poll;  +""", +""" +CREATE TABLE forums ( +    id INTEGER PRIMARY KEY, +    name TEXT NOT NULL, +    parent INTEGER REFERENCES forums(id), +    description TEXT +); +INSERT INTO forums (name,parent,description) values ('apioforum',null, +        '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) +); +INSERT INTO threads_new (id,title,creator,created,updated,forum) +    SELECT id,title,creator,created,updated,1 FROM threads; +DROP TABLE threads; +ALTER TABLE threads_new RENAME TO threads; +COMMIT; +PRAGMA foreign_keys = on; +""", +""" +CREATE VIEW most_recent_posts AS +    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; +""", +""" +CREATE VIEW total_vote_counts AS +    SELECT poll, count(*) AS total_votes FROM votes WHERE current AND NOT is_retraction GROUP BY poll; +""", +""" +PRAGMA foreign_keys = off; +BEGIN TRANSACTION; +CREATE TABLE tags_new ( +	id INTEGER PRIMARY KEY, +	name TEXT NOT NULL, +	text_colour TEXT NOT NULL, +	bg_colour TEXT NOT NULL, +	forum INTEGER NOT NULL REFERENCES forums(id) +); +INSERT INTO tags_new (id,name,text_colour,bg_colour,forum) +	SELECT id,name,text_colour,bg_colour,1 FROM tags; +DROP TABLE tags; +ALTER TABLE tags_new RENAME TO tags; +PRAGMA foreign_keys = on; +""", +""" +CREATE TABLE role_config ( +    role TEXT NOT NULL, +    forum NOT NULL REFERENCES forums(id), +    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 +); + +INSERT INTO role_config (role,forum) VALUES ("approved",1); +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 +); +""", +""" +ALTER TABLE posts ADD COLUMN deleted NOT NULL DEFAULT 0; +""", +""" +ALTER TABLE forums ADD COLUMN unlisted NOT NULL DEFAULT 0; +""", +""" +ALTER TABLE role_config ADD COLUMN p_view_forum INT NOT NULL DEFAULT 1; +""" +] diff --git a/apioforum/forum.py b/apioforum/forum.py index 2931df9..badbd14 100644 --- a/apioforum/forum.py +++ b/apioforum/forum.py @@ -17,6 +17,12 @@ import functools  bp = Blueprint("forum", __name__, url_prefix="/") +class Forum(DbWrapper): +    table = "forums" +    primary_key = "id" +    references = {"parent", Forum} + +  @bp.route("/")  def not_actual_index():      return redirect("/1") diff --git a/apioforum/templates/view_thread.html b/apioforum/templates/view_thread.html index 132fd44..06c110b 100644 --- a/apioforum/templates/view_thread.html +++ b/apioforum/templates/view_thread.html @@ -6,14 +6,14 @@  {% endblock %}  {%block content%} -{% if poll %} -<p>{{poll.title}}</p> +{% if thread.poll %} +<p>{{thread.poll.title}}</p>  <ol> -    {%for opt in poll.options%} +    {%for opt in thread.poll.options%}      <li value="{{opt.option_idx}}"><i>{{opt.text}}</i>: {{opt.num or 0}} votes</li>      {%endfor%}  </ol> -{{ vote_meter(poll) }} +{{ vote_meter(thread.poll) }}  {% endif %}  <div class="thread-top-bar">      <span class="thread-top-bar-a"> @@ -34,7 +34,7 @@  <div class="posts">      {% for post in posts %} -        {% if votes[post.id] %} +        {% if post.vote %}              {% set vote = votes[post.id] %}                      {% set option_idx = vote.option_idx %} @@ -48,7 +48,7 @@  						this post retracted a vote  					{% endif %}                  {% else %} -                    {% set option = poll.options[option_idx-1] %} +                    {% set option = thread.poll.options[option_idx-1] %}                      {% if vote.current %}                          {{post.author}} votes for {{option_idx}}: {{option.text}}                      {% else %} @@ -72,9 +72,9 @@  {% 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") %} +    {% if thread.poll and has_permission(thread.forum, g.user, "p_vote") %}      <fieldset> -        <legend>poll: {{poll.title}}</legend> +        <legend>poll: {{thread.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> @@ -87,7 +87,7 @@          <label for="retractvote">clear current vote</label>          {% endif %} -        {% for opt in poll.options %} +        {% for opt in thread.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> diff --git a/apioforum/thread.py b/apioforum/thread.py index 2fc9dca..d0d7e83 100644 --- a/apioforum/thread.py +++ b/apioforum/thread.py @@ -6,41 +6,26 @@ from flask import (      Blueprint, render_template, abort, request, g, redirect,      url_for, flash, jsonify  ) -from .db import get_db +from .db import get_db, DbWrapper  from .roles import has_permission -from .forum import get_avail_tags +from .forum import get_avail_tags, Forum +from .user import User  bp = Blueprint("thread", __name__, url_prefix="/thread") -def post_jump(thread_id, post_id): -    return url_for("thread.view_thread",thread_id=thread_id)+"#post_"+str(post_id) +class Poll(DbWrapper): +    table = "polls" -@bp.route("/<int:thread_id>") -def view_thread(thread_id): -    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) -    posts = db.execute(""" -        SELECT * FROM posts -        WHERE posts.thread = ? -        ORDER BY created ASC; -        """,(thread_id,)).fetchall() -    tags = db.execute( -        """SELECT tags.* FROM tags -        INNER JOIN thread_tags ON thread_tags.tag = tags.id -        WHERE thread_tags.thread = ? -        ORDER BY tags.id""",(thread_id,)).fetchall() -    poll = None -    votes = None -    if thread['poll'] is not None: -        poll_row= db.execute(""" +    @classmethod +    def get_row(cls, key): +        db = get_db() +        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() +            """,(key,)).fetchone() +        if row == None: +            return None          options = db.execute("""              SELECT poll_options.*, vote_counts.num              FROM poll_options @@ -48,20 +33,66 @@ def view_thread(thread_id):                                          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: +            """,(key,)).fetchall() +        row['options'] = options + +class PollOption(DbWrapper): +    table = "poll_options" +    primary_key = None +    references = {"poll": Poll} + +class Vote(DbWrapper): +    table = "votes" +    references = {"poll": Poll, "user": User} + +class Tag(DbWrapper): +    table = "tags" +    references = {"forum": Forum} + +class Thread(DbWrapper): +    table = "threads" +    references = {"forum": Forum, "poll": Poll} + +    def get_posts(self): +        return Post.query_some(""" +            SELECT * FROM posts +            WHERE thread = ? +            ORDER BY created ASC; +            """,(self,)) + +    def get_tags(self): +        return Tag.query_some(""" +            SELECT tags.* FROM tags +            INNER JOIN thread_tags ON thread_tags.tag = tags.id +            ORDER BY tags.id +            """,(self,)) + +class Post(DbWrapper): +    table = "posts" +    references = {"thread": Thread, "author": User, "vote": Vote} + + +def post_jump(thread_id, post_id): +    return url_for("thread.view_thread",thread_id=thread_id)+"#post_"+str(post_id) + +@bp.route("/<db(Thread):thread>") +def view_thread(thread): +    db = get_db() +    if not has_permission(thread['forum'], g.user, "p_view_threads", False): +        abort(403) +    posts = thread.get_posts() +    tags = thread.get_tags() + +    if g.user is None or thread.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() +        v = Vote.query_one(""" +            SELECT * FROM votes  +            WHERE poll = ?  +                AND user = ?  +                AND current  +                AND NOT is_retraction; +            """,(thread.poll,g.user))          has_voted = v is not None      return render_template( @@ -70,7 +101,6 @@ def view_thread(thread_id):          thread=thread,          tags=tags,          poll=poll, -        votes=votes,          has_voted=has_voted,      ) diff --git a/apioforum/user.py b/apioforum/user.py index bbdd060..b92d8b3 100644 --- a/apioforum/user.py +++ b/apioforum/user.py @@ -5,51 +5,59 @@ from flask import (  )  from werkzeug.security import check_password_hash, generate_password_hash -from .db import get_db +from .db import DbWrapper, get_db  bp = Blueprint("user", __name__, url_prefix="/user") +class User(DbWrapper): +    table = "users" +    primary_key = "username" +     +    def set_password(self, password): +        self.password = generate_password_hash(password) +  @bp.route("/<username>")  def view_user(username):      db = get_db() -    user = db.execute("SELECT * FROM users WHERE username = ?;",(username,)).fetchone() -    if user is None: +    try: +        user = User.fetch(username) +    except KeyError:          abort(404)      posts = db.execute("""          SELECT * FROM posts          WHERE author = ? AND deleted = 0          ORDER BY created DESC  -        LIMIT 25;""",(username,)).fetchall() +        LIMIT 25;""",(user,)).fetchall()      return render_template("view_user.html", user=user, posts=posts)  @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: +    try: +        user = User.fetch(username) +    except KeyError:          abort(404)      if username != g.user:          flash("you cannot modify other people")          return redirect(url_for("user.view_user",username=username)) +    db = get_db()      if request.method == "POST":          err = []          if len(request.form['new_password']) > 0: -            if not check_password_hash(user['password'],request.form['password']): +            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)) +                user.set_password(request.form['new_password'])                  db.commit()                  flash("password changed changefully") -        if request.form['bio'] != user['bio']: +        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)) +                user.bio = request.form['bio']                  db.commit()                  flash("bio updated successfully")  | 
