Browse Source

Merge remote-tracking branch 'upstream/master'

Conflicts:
	flaskbb/templates/forum/topic.html
Casper Van Gheluwe 10 years ago
parent
commit
7fbb8187ed

+ 1 - 0
.travis.yml

@@ -1,6 +1,7 @@
 language: python
 language: python
 python:
 python:
   - "2.7"
   - "2.7"
+  - "3.3"
 # command to install dependencies
 # command to install dependencies
 install:
 install:
   - "pip install -r requirements.txt"
   - "pip install -r requirements.txt"

+ 2 - 1
AUTHORS

@@ -9,4 +9,5 @@ contributors:
 
 
 # Contributors
 # Contributors
 
 
-* None! Be the first :)
+* CasperVg
+* Feel free to join! :)

+ 1 - 1
README.md

@@ -7,7 +7,7 @@
 using the micro framework Flask.
 using the micro framework Flask.
 
 
 
 
-**_Note:_** Do not expect too much activity during the summer holidays - I hope you can understand this :)
+**_Note:_** I'm doing my exchange semester and most of the time I'm busy with traveling and such :). The project is not dead in any way - I'm just really busy.
 
 
 
 
 ## FEATURES
 ## FEATURES

+ 29 - 0
flaskbb/_compat.py

@@ -0,0 +1,29 @@
+"""
+Look here for more information:
+https://github.com/mitsuhiko/flask/blob/master/flask/_compat.py
+"""
+
+import sys
+
+PY2 = sys.version_info[0] == 2
+
+if not PY2:     # pragma: no cover
+    text_type = str
+    string_types = (str,)
+    integer_types = (int, )
+    intern_method = sys.intern
+    range_method = range
+    iterkeys = lambda d: iter(d.keys())
+    itervalues = lambda d: iter(d.values())
+    iteritems = lambda d: iter(d.items())
+    max_integer = sys.maxsize
+else:           # pragma: no cover
+    text_type = unicode
+    string_types = (str, unicode)
+    integer_types = (int, long)
+    intern_method = intern
+    range_method = xrange
+    iterkeys = lambda d: d.iterkeys()
+    itervalues = lambda d: d.itervalues()
+    iteritems = lambda d: d.iteritems()
+    max_integer = sys.maxint

+ 25 - 4
flaskbb/app.py

@@ -11,6 +11,10 @@
 import os
 import os
 import logging
 import logging
 import datetime
 import datetime
+import time
+
+from sqlalchemy import event
+from sqlalchemy.engine import Engine
 
 
 from flask import Flask, request
 from flask import Flask, request
 from flask.ext.login import current_user
 from flask.ext.login import current_user
@@ -26,7 +30,7 @@ from flaskbb.management.views import management
 from flaskbb.forum.views import forum
 from flaskbb.forum.views import forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
 from flaskbb.forum.models import Post, Topic, Category, Forum
 # extensions
 # extensions
-from flaskbb.extensions import db, login_manager, mail, cache, redis, \
+from flaskbb.extensions import db, login_manager, mail, cache, redis_store, \
     debugtoolbar, migrate, themes, plugin_manager
     debugtoolbar, migrate, themes, plugin_manager
 from flask.ext.whooshalchemy import whoosh_index
 from flask.ext.whooshalchemy import whoosh_index
 # various helpers
 # various helpers
@@ -70,7 +74,9 @@ def configure_blueprints(app):
     app.register_blueprint(forum, url_prefix=app.config["FORUM_URL_PREFIX"])
     app.register_blueprint(forum, url_prefix=app.config["FORUM_URL_PREFIX"])
     app.register_blueprint(user, url_prefix=app.config["USER_URL_PREFIX"])
     app.register_blueprint(user, url_prefix=app.config["USER_URL_PREFIX"])
     app.register_blueprint(auth, url_prefix=app.config["AUTH_URL_PREFIX"])
     app.register_blueprint(auth, url_prefix=app.config["AUTH_URL_PREFIX"])
-    app.register_blueprint(management, url_prefix=app.config["ADMIN_URL_PREFIX"])
+    app.register_blueprint(
+        management, url_prefix=app.config["ADMIN_URL_PREFIX"]
+    )
 
 
 
 
 def configure_extensions(app):
 def configure_extensions(app):
@@ -100,7 +106,7 @@ def configure_extensions(app):
     themes.init_themes(app, app_identifier="flaskbb")
     themes.init_themes(app, app_identifier="flaskbb")
 
 
     # Flask-And-Redis
     # Flask-And-Redis
-    redis.init_app(app)
+    redis_store.init_app(app)
 
 
     # Flask-WhooshAlchemy
     # Flask-WhooshAlchemy
     with app.app_context():
     with app.app_context():
@@ -121,7 +127,7 @@ def configure_extensions(app):
         Loads the user. Required by the `login` extension
         Loads the user. Required by the `login` extension
         """
         """
         unread_count = db.session.query(db.func.count(PrivateMessage.id)).\
         unread_count = db.session.query(db.func.count(PrivateMessage.id)).\
-            filter(PrivateMessage.unread == True,
+            filter(PrivateMessage.unread,
                    PrivateMessage.user_id == id).subquery()
                    PrivateMessage.user_id == id).subquery()
         u = db.session.query(User, unread_count).filter(User.id == id).first()
         u = db.session.query(User, unread_count).filter(User.id == id).first()
 
 
@@ -264,3 +270,18 @@ def configure_logging(app):
         mail_handler.setLevel(logging.ERROR)
         mail_handler.setLevel(logging.ERROR)
         mail_handler.setFormatter(formatter)
         mail_handler.setFormatter(formatter)
         app.logger.addHandler(mail_handler)
         app.logger.addHandler(mail_handler)
+
+    if app.config["SQLALCHEMY_ECHO"]:
+        # Ref: http://stackoverflow.com/a/8428546
+        @event.listens_for(Engine, "before_cursor_execute")
+        def before_cursor_execute(conn, cursor, statement,
+                                  parameters, context, executemany):
+            context._query_start_time = time.time()
+
+        @event.listens_for(Engine, "after_cursor_execute")
+        def after_cursor_execute(conn, cursor, statement,
+                                 parameters, context, executemany):
+            total = time.time() - context._query_start_time
+
+            # Modification for StackOverflow: times in milliseconds
+            app.logger.debug("Total Time: %.02fms" % (total*1000))

+ 20 - 18
flaskbb/auth/forms.py

@@ -11,8 +11,9 @@
 from datetime import datetime
 from datetime import datetime
 
 
 from flask.ext.wtf import Form, RecaptchaField
 from flask.ext.wtf import Form, RecaptchaField
-from wtforms import TextField, PasswordField, BooleanField, HiddenField
-from wtforms.validators import Required, Email, EqualTo, regexp, ValidationError
+from wtforms import StringField, PasswordField, BooleanField, HiddenField
+from wtforms.validators import (DataRequired, Email, EqualTo, regexp,
+                                ValidationError)
 
 
 from flaskbb.user.models import User
 from flaskbb.user.models import User
 
 
@@ -22,29 +23,29 @@ is_username = regexp(USERNAME_RE,
 
 
 
 
 class LoginForm(Form):
 class LoginForm(Form):
-    login = TextField("Username or E-Mail", validators=[
-        Required(message="You must provide an email adress or username")])
+    login = StringField("Username or E-Mail", validators=[
+        DataRequired(message="You must provide an email adress or username")])
 
 
     password = PasswordField("Password", validators=[
     password = PasswordField("Password", validators=[
-        Required(message="Password required")])
+        DataRequired(message="Password required")])
 
 
     remember_me = BooleanField("Remember Me", default=False)
     remember_me = BooleanField("Remember Me", default=False)
 
 
 
 
 class RegisterForm(Form):
 class RegisterForm(Form):
-    username = TextField("Username", validators=[
-        Required(message="Username required"),
+    username = StringField("Username", validators=[
+        DataRequired(message="Username required"),
         is_username])
         is_username])
 
 
-    email = TextField("E-Mail", validators=[
-        Required(message="Email adress required"),
+    email = StringField("E-Mail", validators=[
+        DataRequired(message="Email adress required"),
         Email(message="This email is invalid")])
         Email(message="This email is invalid")])
 
 
     password = PasswordField("Password", validators=[
     password = PasswordField("Password", validators=[
-        Required(message="Password required")])
+        DataRequired(message="Password required")])
 
 
     confirm_password = PasswordField("Confirm Password", validators=[
     confirm_password = PasswordField("Confirm Password", validators=[
-        Required(message="Confirm Password required"),
+        DataRequired(message="Confirm Password required"),
         EqualTo("password", message="Passwords do not match")])
         EqualTo("password", message="Passwords do not match")])
 
 
     accept_tos = BooleanField("Accept Terms of Service", default=True)
     accept_tos = BooleanField("Accept Terms of Service", default=True)
@@ -73,27 +74,28 @@ class RegisterRecaptchaForm(RegisterForm):
 
 
 
 
 class ReauthForm(Form):
 class ReauthForm(Form):
-    password = PasswordField('Password', [Required()])
+    password = PasswordField('Password', valdidators=[
+        DataRequired()])
 
 
 
 
 class ForgotPasswordForm(Form):
 class ForgotPasswordForm(Form):
-    email = TextField('Email', validators=[
-        Required(message="Email reguired"),
+    email = StringField('Email', validators=[
+        DataRequired(message="Email reguired"),
         Email()])
         Email()])
 
 
 
 
 class ResetPasswordForm(Form):
 class ResetPasswordForm(Form):
     token = HiddenField('Token')
     token = HiddenField('Token')
 
 
-    email = TextField('Email', validators=[
-        Required(),
+    email = StringField('Email', validators=[
+        DataRequired(),
         Email()])
         Email()])
 
 
     password = PasswordField('Password', validators=[
     password = PasswordField('Password', validators=[
-        Required()])
+        DataRequired()])
 
 
     confirm_password = PasswordField('Confirm password', validators=[
     confirm_password = PasswordField('Confirm password', validators=[
-        Required(),
+        DataRequired(),
         EqualTo('password', message='Passwords must match')])
         EqualTo('password', message='Passwords must match')])
 
 
     def validate_email(self, field):
     def validate_email(self, field):

+ 3 - 4
flaskbb/configs/default.py

@@ -79,11 +79,10 @@ class DefaultConfig(object):
     # Where to logger should send the emails to
     # Where to logger should send the emails to
     ADMINS = ["admin@example.org"]
     ADMINS = ["admin@example.org"]
 
 
-    ## Flask-And-Redis
+    # Flask-Redis
     REDIS_ENABLED = False
     REDIS_ENABLED = False
-    REDIS_HOST = 'localhost'
-    REDIS_PORT = 6379
-    REDIS_DB = 0
+    REDIS_URL = "redis://:password@localhost:6379"
+    REDIS_DATABASE = 0
 
 
     FORUM_URL_PREFIX = ""
     FORUM_URL_PREFIX = ""
     USER_URL_PREFIX = "/user"
     USER_URL_PREFIX = "/user"

+ 3 - 4
flaskbb/configs/production.py.example

@@ -80,11 +80,10 @@ class ProductionConfig(DefaultConfig):
     INFO_LOG = "info.log"
     INFO_LOG = "info.log"
     ERROR_LOG = "error.log"
     ERROR_LOG = "error.log"
 
 
-    # Redis
+    # Flask-Redis
     REDIS_ENABLED = False
     REDIS_ENABLED = False
-    REDIS_HOST = 'localhost'
-    REDIS_PORT = 6379
-    REDIS_DB = 0
+    REDIS_URL = "redis://:password@localhost:6379"
+    REDIS_DATABASE = 0
 
 
     # URL Prefixes.
     # URL Prefixes.
     FORUM_URL_PREFIX = ""
     FORUM_URL_PREFIX = ""

+ 2 - 6
flaskbb/email.py

@@ -30,12 +30,8 @@ def send_reset_token(user, token):
     )
     )
 
 
 
 
-def send_email(subject, recipients, text_body, html_body, sender=""):
-    if not sender:
-        msg = Message(subject, recipients=recipients)
-    else:
-        msg = Message(subject, recipients=recipients, sender=sender)
-
+def send_email(subject, recipients, text_body, html_body, sender=None):
+    msg = Message(subject, recipients=recipients, sender=sender)
     msg.body = text_body
     msg.body = text_body
     msg.html = html_body
     msg.html = html_body
     mail.send(msg)
     mail.send(msg)

+ 1 - 1
flaskbb/extensions.py

@@ -32,7 +32,7 @@ mail = Mail()
 cache = Cache()
 cache = Cache()
 
 
 # Redis
 # Redis
-redis = Redis()
+redis_store = Redis()
 
 
 # Debugtoolbar
 # Debugtoolbar
 debugtoolbar = DebugToolbarExtension()
 debugtoolbar = DebugToolbarExtension()

+ 12 - 0
flaskbb/fixtures/settings.py

@@ -14,6 +14,11 @@ from flask.ext.themes2 import get_themes_list
 def available_themes():
 def available_themes():
     return [(theme.identifier, theme.name) for theme in get_themes_list()]
     return [(theme.identifier, theme.name) for theme in get_themes_list()]
 
 
+
+def available_markups():
+    return [('bbcode', 'BBCode'), ('markdown', 'Markdown')]
+
+
 fixture = (
 fixture = (
     # Settings Group
     # Settings Group
     ('general', {
     ('general', {
@@ -79,6 +84,13 @@ fixture = (
                 'extra':        {'min': 0},
                 'extra':        {'min': 0},
                 'name':         "Tracker length",
                 'name':         "Tracker length",
                 'description':  "The days for how long the forum should deal with unread topics. 0 to disable it."
                 'description':  "The days for how long the forum should deal with unread topics. 0 to disable it."
+            }),
+            ('markup_type', {
+                'value':        "bbcode",
+                'value_type':   "select",
+                'extra':        {'choices': available_markups},
+                'name':         "Post markup",
+                'description':  "Select post markup type."
             })
             })
         ),
         ),
     }),
     }),

+ 15 - 14
flaskbb/forum/forms.py

@@ -9,8 +9,9 @@
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
 from flask.ext.wtf import Form
 from flask.ext.wtf import Form
-from wtforms import TextAreaField, TextField, SelectMultipleField, BooleanField
-from wtforms.validators import Required, Optional, Length
+from wtforms import (TextAreaField, StringField, SelectMultipleField,
+                     BooleanField)
+from wtforms.validators import DataRequired, Optional, Length
 
 
 from flaskbb.forum.models import Topic, Post, Report, Forum
 from flaskbb.forum.models import Topic, Post, Report, Forum
 from flaskbb.user.models import User
 from flaskbb.user.models import User
@@ -18,7 +19,7 @@ from flaskbb.user.models import User
 
 
 class QuickreplyForm(Form):
 class QuickreplyForm(Form):
     content = TextAreaField("Quickreply", validators=[
     content = TextAreaField("Quickreply", validators=[
-        Required(message="You cannot post a reply without content.")])
+        DataRequired(message="You cannot post a reply without content.")])
 
 
     def save(self, user, topic):
     def save(self, user, topic):
         post = Post(**self.data)
         post = Post(**self.data)
@@ -27,7 +28,7 @@ class QuickreplyForm(Form):
 
 
 class ReplyForm(Form):
 class ReplyForm(Form):
     content = TextAreaField("Content", validators=[
     content = TextAreaField("Content", validators=[
-        Required(message="You cannot post a reply without content.")])
+        DataRequired(message="You cannot post a reply without content.")])
 
 
     track_topic = BooleanField("Track this topic", default=False, validators=[
     track_topic = BooleanField("Track this topic", default=False, validators=[
         Optional()])
         Optional()])
@@ -41,11 +42,11 @@ class ReplyForm(Form):
 
 
 
 
 class NewTopicForm(ReplyForm):
 class NewTopicForm(ReplyForm):
-    title = TextField("Topic Title", validators=[
-        Required(message="A topic title is required")])
+    title = StringField("Topic Title", validators=[
+        DataRequired(message="A topic title is required")])
 
 
     content = TextAreaField("Content", validators=[
     content = TextAreaField("Content", validators=[
-        Required(message="You cannot post a reply without content.")])
+        DataRequired(message="You cannot post a reply without content.")])
 
 
     track_topic = BooleanField("Track this topic", default=False, validators=[
     track_topic = BooleanField("Track this topic", default=False, validators=[
         Optional()])
         Optional()])
@@ -61,8 +62,8 @@ class NewTopicForm(ReplyForm):
 
 
 class ReportForm(Form):
 class ReportForm(Form):
     reason = TextAreaField("Reason", validators=[
     reason = TextAreaField("Reason", validators=[
-        Required(message="Please insert a reason why you want to report this \
-                          post")
+        DataRequired(message="Please insert a reason why you want to report \
+                              this post")
     ])
     ])
 
 
     def save(self, user, post):
     def save(self, user, post):
@@ -71,7 +72,7 @@ class ReportForm(Form):
 
 
 
 
 class UserSearchForm(Form):
 class UserSearchForm(Form):
-    search_query = TextField("Search", validators=[
+    search_query = StringField("Search", validators=[
         Optional(), Length(min=3, max=50)
         Optional(), Length(min=3, max=50)
     ])
     ])
 
 
@@ -81,12 +82,12 @@ class UserSearchForm(Form):
 
 
 
 
 class SearchPageForm(Form):
 class SearchPageForm(Form):
-    search_query = TextField("Criteria", validators=[
-        Required(), Length(min=3, max=50)])
+    search_query = StringField("Criteria", validators=[
+        DataRequired(), Length(min=3, max=50)])
 
 
     search_types = SelectMultipleField("Content", validators=[
     search_types = SelectMultipleField("Content", validators=[
-        Required()], choices=[('post', 'Post'), ('topic', 'Topic'),
-                              ('forum', 'Forum'), ('user', 'Users')])
+        DataRequired()], choices=[('post', 'Post'), ('topic', 'Topic'),
+                                  ('forum', 'Forum'), ('user', 'Users')])
 
 
     def get_results(self):
     def get_results(self):
         # Because the DB is not yet initialized when this form is loaded,
         # Because the DB is not yet initialized when this form is loaded,

+ 34 - 4
flaskbb/forum/models.py

@@ -156,7 +156,7 @@ class Post(db.Model):
                                        use_alter=True,
                                        use_alter=True,
                                        name="fk_post_topic_id",
                                        name="fk_post_topic_id",
                                        ondelete="CASCADE"))
                                        ondelete="CASCADE"))
-    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
+    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
     username = db.Column(db.String(200), nullable=False)
     username = db.Column(db.String(200), nullable=False)
     content = db.Column(db.Text, nullable=False)
     content = db.Column(db.Text, nullable=False)
     date_created = db.Column(db.DateTime, default=datetime.utcnow())
     date_created = db.Column(db.DateTime, default=datetime.utcnow())
@@ -211,7 +211,13 @@ class Post(db.Model):
 
 
             # Now lets update the last post id
             # Now lets update the last post id
             topic.last_post_id = self.id
             topic.last_post_id = self.id
+
+            # Update the last post info for the forum
             topic.forum.last_post_id = self.id
             topic.forum.last_post_id = self.id
+            topic.forum.last_post_title = topic.title
+            topic.forum.last_post_user_id = user.id
+            topic.forum.last_post_username = user.username
+            topic.forum.last_post_created = datetime.utcnow()
 
 
             # Update the post counts
             # Update the post counts
             user.post_count += 1
             user.post_count += 1
@@ -301,7 +307,7 @@ class Topic(db.Model):
                                 foreign_keys=[last_post_id])
                                 foreign_keys=[last_post_id])
 
 
     # One-to-many
     # One-to-many
-    posts = db.relationship("Post", backref="topic", lazy="joined",
+    posts = db.relationship("Post", backref="topic", lazy="dynamic",
                             primaryjoin="Post.topic_id == Topic.id",
                             primaryjoin="Post.topic_id == Topic.id",
                             cascade="all, delete-orphan", post_update=True)
                             cascade="all, delete-orphan", post_update=True)
 
 
@@ -493,6 +499,7 @@ class Topic(db.Model):
 
 
         :param post: The post object which is connected to the topic
         :param post: The post object which is connected to the topic
         """
         """
+
         # Updates the topic
         # Updates the topic
         if self.id:
         if self.id:
             db.session.add(self)
             db.session.add(self)
@@ -507,6 +514,8 @@ class Topic(db.Model):
         # Set the last_updated time. Needed for the readstracker
         # Set the last_updated time. Needed for the readstracker
         self.last_updated = datetime.utcnow()
         self.last_updated = datetime.utcnow()
 
 
+        self.date_created = datetime.utcnow()
+
         # Insert and commit the topic
         # Insert and commit the topic
         db.session.add(self)
         db.session.add(self)
         db.session.commit()
         db.session.commit()
@@ -543,6 +552,10 @@ class Topic(db.Model):
             # There is no second last post
             # There is no second last post
             except IndexError:
             except IndexError:
                 self.forum.last_post_id = None
                 self.forum.last_post_id = None
+                self.forum.last_post_title = None
+                self.forum.last_post_user_id = None
+                self.forum.last_post_username = None
+                self.forum.last_post_created = None
 
 
             # Commit the changes
             # Commit the changes
             db.session.commit()
             db.session.commit()
@@ -597,8 +610,14 @@ class Forum(db.Model):
     last_post = db.relationship("Post", backref="last_post_forum",
     last_post = db.relationship("Post", backref="last_post_forum",
                                 uselist=False, foreign_keys=[last_post_id])
                                 uselist=False, foreign_keys=[last_post_id])
 
 
+    # Not nice, but needed to improve the performance
+    last_post_title = db.Column(db.String(255))
+    last_post_user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
+    last_post_username = db.Column(db.String(255))
+    last_post_created = db.Column(db.DateTime, default=datetime.utcnow())
+
     # One-to-many
     # One-to-many
-    topics = db.relationship("Topic", backref="forum", lazy="joined",
+    topics = db.relationship("Topic", backref="forum", lazy="dynamic",
                              cascade="all, delete-orphan")
                              cascade="all, delete-orphan")
 
 
     # Many-to-many
     # Many-to-many
@@ -621,6 +640,11 @@ class Forum(db.Model):
             return self.external
             return self.external
         return url_for("forum.view_forum", forum_id=self.id, slug=self.slug)
         return url_for("forum.view_forum", forum_id=self.id, slug=self.slug)
 
 
+    @property
+    def last_post_url(self):
+        """Returns the url for the last post in the forum"""
+        return url_for("forum.view_post", post_id=self.last_post_id)
+
     # Methods
     # Methods
     def __repr__(self):
     def __repr__(self):
         """Set to a unique key specific to the object in the database.
         """Set to a unique key specific to the object in the database.
@@ -671,6 +695,11 @@ class Forum(db.Model):
         if not user.is_authenticated() or topicsread is None:
         if not user.is_authenticated() or topicsread is None:
             return False
             return False
 
 
+        read_cutoff = None
+        if flaskbb_config['TRACKER_LENGTH'] > 0:
+            read_cutoff = datetime.utcnow() - timedelta(
+                days=flaskbb_config['TRACKER_LENGTH'])
+
         # fetch the unread posts in the forum
         # fetch the unread posts in the forum
         unread_count = Topic.query.\
         unread_count = Topic.query.\
             outerjoin(TopicsRead,
             outerjoin(TopicsRead,
@@ -680,6 +709,7 @@ class Forum(db.Model):
                       db.and_(ForumsRead.forum_id == Topic.forum_id,
                       db.and_(ForumsRead.forum_id == Topic.forum_id,
                               ForumsRead.user_id == user.id)).\
                               ForumsRead.user_id == user.id)).\
             filter(Topic.forum_id == self.id,
             filter(Topic.forum_id == self.id,
+                   Topic.last_updated > read_cutoff,
                    db.or_(TopicsRead.last_read == None,
                    db.or_(TopicsRead.last_read == None,
                           TopicsRead.last_read < Topic.last_updated)).\
                           TopicsRead.last_read < Topic.last_updated)).\
             count()
             count()
@@ -706,7 +736,7 @@ class Forum(db.Model):
             forumsread.save()
             forumsread.save()
             return True
             return True
 
 
-        # Nothing updated, because there are still more than 0 unread topics
+        # Nothing updated, because there are still more than 0 unread topicsread
         return False
         return False
 
 
     def save(self, moderators=None):
     def save(self, moderators=None):

+ 103 - 96
flaskbb/forum/views.py

@@ -17,7 +17,8 @@ from flask.ext.login import login_required, current_user
 
 
 from flaskbb.extensions import db
 from flaskbb.extensions import db
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
-from flaskbb.utils.helpers import get_online_users, time_diff, render_template
+from flaskbb.utils.helpers import (get_online_users, time_diff, render_template,
+                                   format_quote)
 from flaskbb.utils.permissions import (can_post_reply, can_post_topic,
 from flaskbb.utils.permissions import (can_post_reply, can_post_topic,
                                        can_delete_topic, can_delete_post,
                                        can_delete_topic, can_delete_post,
                                        can_edit_post, can_moderate)
                                        can_edit_post, can_moderate)
@@ -76,16 +77,21 @@ def view_category(category_id, slug=None):
 def view_forum(forum_id, slug=None):
 def view_forum(forum_id, slug=None):
     page = request.args.get('page', 1, type=int)
     page = request.args.get('page', 1, type=int)
 
 
-    forum, forumsread = Forum.get_forum(forum_id=forum_id, user=current_user)
+    forum_instance, forumsread = Forum.get_forum(forum_id=forum_id,
+                                                 user=current_user)
 
 
-    if forum.external:
-        return redirect(forum.external)
+    if forum_instance.external:
+        return redirect(forum_instance.external)
 
 
-    topics = Forum.get_topics(forum_id=forum.id, user=current_user, page=page,
-                              per_page=flaskbb_config["TOPICS_PER_PAGE"])
+    topics = Forum.get_topics(
+        forum_id=forum_instance.id, user=current_user, page=page,
+        per_page=flaskbb_config["TOPICS_PER_PAGE"]
+    )
 
 
-    return render_template("forum/forum.html", forum=forum, topics=topics,
-                           forumsread=forumsread,)
+    return render_template(
+        "forum/forum.html", forum=forum_instance,
+        topics=topics, forumsread=forumsread,
+    )
 
 
 
 
 @forum.route("/topic/<int:topic_id>", methods=["POST", "GET"])
 @forum.route("/topic/<int:topic_id>", methods=["POST", "GET"])
@@ -93,13 +99,20 @@ def view_forum(forum_id, slug=None):
 def view_topic(topic_id, slug=None):
 def view_topic(topic_id, slug=None):
     page = request.args.get('page', 1, type=int)
     page = request.args.get('page', 1, type=int)
 
 
+    # Fetch some information about the topic
     topic = Topic.query.filter_by(id=topic_id).first()
     topic = Topic.query.filter_by(id=topic_id).first()
-    posts = Post.query.filter_by(topic_id=topic.id).\
-        order_by(Post.id.asc()).\
-        paginate(page, flaskbb_config['POSTS_PER_PAGE'], False)
 
 
     # Count the topic views
     # Count the topic views
     topic.views += 1
     topic.views += 1
+    topic.save()
+
+    # fetch the posts in the topic
+    posts = Post.query.\
+        join(User, Post.user_id == User.id).\
+        filter(Post.topic_id == topic.id).\
+        add_entity(User).\
+        order_by(Post.id.asc()).\
+        paginate(page, flaskbb_config['POSTS_PER_PAGE'], False)
 
 
     # Update the topicsread status if the user hasn't read it
     # Update the topicsread status if the user hasn't read it
     forumsread = None
     forumsread = None
@@ -109,19 +122,14 @@ def view_topic(topic_id, slug=None):
                       forum_id=topic.forum.id).first()
                       forum_id=topic.forum.id).first()
 
 
     topic.update_read(current_user, topic.forum, forumsread)
     topic.update_read(current_user, topic.forum, forumsread)
-    topic.save()
 
 
     form = None
     form = None
 
 
-    if not topic.locked \
-        and not topic.forum.locked \
-        and can_post_reply(user=current_user,
-                           forum=topic.forum):
-
-            form = QuickreplyForm()
-            if form.validate_on_submit():
-                post = form.save(current_user, topic)
-                return view_post(post.id)
+    if can_post_reply(user=current_user, topic=topic):
+        form = QuickreplyForm()
+        if form.validate_on_submit():
+            post = form.save(current_user, topic)
+            return view_post(post.id)
 
 
     return render_template("forum/topic.html", topic=topic, posts=posts,
     return render_template("forum/topic.html", topic=topic, posts=posts,
                            last_seen=time_diff(), form=form)
                            last_seen=time_diff(), form=form)
@@ -145,12 +153,7 @@ def view_post(post_id):
 @forum.route("/<int:forum_id>-<slug>/topic/new", methods=["POST", "GET"])
 @forum.route("/<int:forum_id>-<slug>/topic/new", methods=["POST", "GET"])
 @login_required
 @login_required
 def new_topic(forum_id, slug=None):
 def new_topic(forum_id, slug=None):
-    forum = Forum.query.filter_by(id=forum_id).first_or_404()
-
-    if forum.locked:
-        flash("This forum is locked; you cannot submit new topics or posts.",
-              "danger")
-        return redirect(forum.url)
+    forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
 
 
     if not can_post_topic(user=current_user, forum=forum):
     if not can_post_topic(user=current_user, forum=forum):
         flash("You do not have the permissions to create a new topic.",
         flash("You do not have the permissions to create a new topic.",
@@ -160,13 +163,18 @@ def new_topic(forum_id, slug=None):
     form = NewTopicForm()
     form = NewTopicForm()
     if form.validate_on_submit():
     if form.validate_on_submit():
         if request.form['button'] == 'preview':
         if request.form['button'] == 'preview':
-            return render_template("forum/new_topic.html", forum=forum, form=form, preview=form.content.data)
+            return render_template(
+                "forum/new_topic.html", forum=forum_instance,
+                form=form, preview=form.content.data
+            )
         else:
         else:
-            topic = form.save(current_user, forum)
+            topic = form.save(current_user, forum_instance)
 
 
             # redirect to the new topic
             # redirect to the new topic
             return redirect(url_for('forum.view_topic', topic_id=topic.id))
             return redirect(url_for('forum.view_topic', topic_id=topic.id))
-    return render_template("forum/new_topic.html", forum=forum, form=form)
+    return render_template(
+        "forum/new_topic.html", forum=forum_instance, form=form
+    )
 
 
 
 
 @forum.route("/topic/<int:topic_id>/delete")
 @forum.route("/topic/<int:topic_id>/delete")
@@ -175,8 +183,7 @@ def new_topic(forum_id, slug=None):
 def delete_topic(topic_id, slug=None):
 def delete_topic(topic_id, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
 
-    if not can_delete_topic(user=current_user, forum=topic.forum,
-                            post_user_id=topic.first_post.user_id):
+    if not can_delete_topic(user=current_user, topic=topic):
 
 
         flash("You do not have the permissions to delete the topic", "danger")
         flash("You do not have the permissions to delete the topic", "danger")
         return redirect(topic.forum.url)
         return redirect(topic.forum.url)
@@ -223,23 +230,28 @@ def unlock_topic(topic_id, slug=None):
 
 
 
 
 @forum.route("/topic/<int:topic_id>/move/<int:forum_id>")
 @forum.route("/topic/<int:topic_id>/move/<int:forum_id>")
-@forum.route("/topic/<int:topic_id>-<topic_slug>/move/<int:forum_id>-<forum_slug>")
+@forum.route(
+    "/topic/<int:topic_id>-<topic_slug>/move/<int:forum_id>-<forum_slug>"
+)
 @login_required
 @login_required
 def move_topic(topic_id, forum_id, topic_slug=None, forum_slug=None):
 def move_topic(topic_id, forum_id, topic_slug=None, forum_slug=None):
-    forum = Forum.query.filter_by(id=forum_id).first_or_404()
+    forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
 
     # TODO: Bulk move
     # TODO: Bulk move
 
 
     if not can_moderate(user=current_user, forum=topic.forum):
     if not can_moderate(user=current_user, forum=topic.forum):
         flash("Yo do not have the permissions to move this topic", "danger")
         flash("Yo do not have the permissions to move this topic", "danger")
-        return redirect(forum.url)
+        return redirect(forum_instance.url)
 
 
-    if not topic.move(forum):
-        flash("Could not move the topic to forum %s" % forum.title, "danger")
+    if not topic.move(forum_instance):
+        flash(
+            "Could not move the topic to forum %s" % forum_instance.title,
+            "danger"
+        )
         return redirect(topic.url)
         return redirect(topic.url)
 
 
-    flash("Topic was moved to forum %s" % forum.title, "success")
+    flash("Topic was moved to forum %s" % forum_instance.title, "success")
     return redirect(topic.url)
     return redirect(topic.url)
 
 
 
 
@@ -247,21 +259,22 @@ def move_topic(topic_id, forum_id, topic_slug=None, forum_slug=None):
 @forum.route("/topic/<int:old_id>-<old_slug>/merge/<int:new_id>-<new_slug>")
 @forum.route("/topic/<int:old_id>-<old_slug>/merge/<int:new_id>-<new_slug>")
 @login_required
 @login_required
 def merge_topic(old_id, new_id, old_slug=None, new_slug=None):
 def merge_topic(old_id, new_id, old_slug=None, new_slug=None):
-    old_topic = Topic.query.filter_by(id=old_id).first_or_404()
-    new_topic = Topic.query.filter_by(id=new_id).first_or_404()
+    _old_topic = Topic.query.filter_by(id=old_id).first_or_404()
+    _new_topic = Topic.query.filter_by(id=new_id).first_or_404()
 
 
     # TODO: Bulk merge
     # TODO: Bulk merge
 
 
-    if not can_moderate(user=current_user, forum=topic.forum):
+    # Looks to me that the user should have permissions on both forums, right?
+    if not can_moderate(user=current_user, forum=_old_topic.forum):
         flash("Yo do not have the permissions to merge this topic", "danger")
         flash("Yo do not have the permissions to merge this topic", "danger")
-        return redirect(old_topic.url)
+        return redirect(_old_topic.url)
 
 
-    if not old_topic.merge(new_topic):
+    if not _old_topic.merge(_new_topic):
         flash("Could not merge the topic.", "danger")
         flash("Could not merge the topic.", "danger")
-        return redirect(old_topic.url)
+        return redirect(_old_topic.url)
 
 
     flash("Topic succesfully merged.", "success")
     flash("Topic succesfully merged.", "success")
-    return redirect(new_topic.url)
+    return redirect(_new_topic.url)
 
 
 
 
 @forum.route("/topic/<int:topic_id>/post/new", methods=["POST", "GET"])
 @forum.route("/topic/<int:topic_id>/post/new", methods=["POST", "GET"])
@@ -270,23 +283,17 @@ def merge_topic(old_id, new_id, old_slug=None, new_slug=None):
 def new_post(topic_id, slug=None):
 def new_post(topic_id, slug=None):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
 
 
-    if topic.forum.locked:
-        flash("This forum is locked; you cannot submit new topics or posts.",
-              "danger")
-        return redirect(topic.forum.url)
-
-    if topic.locked:
-        flash("The topic is locked.", "danger")
-        return redirect(topic.forum.url)
-
-    if not can_post_reply(user=current_user, forum=topic.forum):
-        flash("You do not have the permissions to delete the topic", "danger")
+    if not can_post_reply(user=current_user, topic=topic):
+        flash("You do not have the permissions to post here", "danger")
         return redirect(topic.forum.url)
         return redirect(topic.forum.url)
 
 
     form = ReplyForm()
     form = ReplyForm()
     if form.validate_on_submit():
     if form.validate_on_submit():
         if request.form['button'] == 'preview':
         if request.form['button'] == 'preview':
-            return render_template("forum/new_post.html", topic=topic, form=form, preview=form.content.data)
+            return render_template(
+                "forum/new_post.html", topic=topic,
+                form=form, preview=form.content.data
+            )
         else:
         else:
             post = form.save(current_user, topic)
             post = form.save(current_user, topic)
             return view_post(post.id)
             return view_post(post.id)
@@ -294,34 +301,30 @@ def new_post(topic_id, slug=None):
     return render_template("forum/new_post.html", topic=topic, form=form)
     return render_template("forum/new_post.html", topic=topic, form=form)
 
 
 
 
-@forum.route("/topic/<int:topic_id>/post/<int:post_id>/reply", methods=["POST", "GET"])
+@forum.route(
+    "/topic/<int:topic_id>/post/<int:post_id>/reply", methods=["POST", "GET"]
+)
 @login_required
 @login_required
 def reply_post(topic_id, post_id):
 def reply_post(topic_id, post_id):
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     topic = Topic.query.filter_by(id=topic_id).first_or_404()
     post = Post.query.filter_by(id=post_id).first_or_404()
     post = Post.query.filter_by(id=post_id).first_or_404()
 
 
-    if post.topic.forum.locked:
-        flash("This forum is locked; you cannot submit new topics or posts.",
-              "danger")
-        return redirect(post.topic.forum.url)
-
-    if post.topic.locked:
-        flash("The topic is locked.", "danger")
-        return redirect(post.topic.forum.url)
-
-    if not can_post_reply(user=current_user, forum=topic.forum):
+    if not can_post_reply(user=current_user, topic=topic):
         flash("You do not have the permissions to post in this topic", "danger")
         flash("You do not have the permissions to post in this topic", "danger")
         return redirect(topic.forum.url)
         return redirect(topic.forum.url)
 
 
     form = ReplyForm()
     form = ReplyForm()
     if form.validate_on_submit():
     if form.validate_on_submit():
         if request.form['button'] == 'preview':
         if request.form['button'] == 'preview':
-            return render_template("forum/new_post.html", topic=topic, form=form, preview=form.content.data)
+            return render_template(
+                "forum/new_post.html", topic=topic,
+                form=form, preview=form.content.data
+            )
         else:
         else:
             form.save(current_user, topic)
             form.save(current_user, topic)
             return redirect(post.topic.url)
             return redirect(post.topic.url)
     else:
     else:
-        form.content.data = '[quote]{}[/quote]'.format(post.content)
+        form.content.data = format_quote(post)
 
 
     return render_template("forum/new_post.html", topic=post.topic, form=form)
     return render_template("forum/new_post.html", topic=post.topic, form=form)
 
 
@@ -331,24 +334,17 @@ def reply_post(topic_id, post_id):
 def edit_post(post_id):
 def edit_post(post_id):
     post = Post.query.filter_by(id=post_id).first_or_404()
     post = Post.query.filter_by(id=post_id).first_or_404()
 
 
-    if post.topic.forum.locked:
-        flash("This forum is locked; you cannot submit new topics or posts.",
-              "danger")
-        return redirect(post.topic.forum.url)
-
-    if post.topic.locked:
-        flash("The topic is locked.", "danger")
-        return redirect(post.topic.forum.url)
-
-    if not can_edit_post(user=current_user, forum=post.topic.forum,
-                         post_user_id=post.user_id):
+    if not can_edit_post(user=current_user, post=post):
         flash("You do not have the permissions to edit this post", "danger")
         flash("You do not have the permissions to edit this post", "danger")
         return redirect(post.topic.url)
         return redirect(post.topic.url)
 
 
     form = ReplyForm()
     form = ReplyForm()
     if form.validate_on_submit():
     if form.validate_on_submit():
         if request.form['button'] == 'preview':
         if request.form['button'] == 'preview':
-            return render_template("forum/new_post.html", topic=post.topic, form=form, preview=form.content.data)
+            return render_template(
+                "forum/new_post.html", topic=post.topic,
+                form=form, preview=form.content.data
+            )
         else:
         else:
             form.populate_obj(post)
             form.populate_obj(post)
             post.date_modified = datetime.datetime.utcnow()
             post.date_modified = datetime.datetime.utcnow()
@@ -368,17 +364,20 @@ def delete_post(post_id, slug=None):
 
 
     # TODO: Bulk delete
     # TODO: Bulk delete
 
 
-    if not can_delete_post(user=current_user, forum=post.topic.forum,
-                           post_user_id=post.user_id):
+    if not can_delete_post(user=current_user, post=post):
         flash("You do not have the permissions to edit this post", "danger")
         flash("You do not have the permissions to edit this post", "danger")
         return redirect(post.topic.url)
         return redirect(post.topic.url)
 
 
+    first_post = post.first_post
+    topic_url = post.topic.url
+    forum_url = post.topic.forum.url
+
     post.delete()
     post.delete()
 
 
     # If the post was the first post in the topic, redirect to the forums
     # If the post was the first post in the topic, redirect to the forums
-    if post.first_post:
-        return redirect(post.topic.forum.url)
-    return redirect(post.topic.url)
+    if first_post:
+        return redirect(forum_url)
+    return redirect(topic_url)
 
 
 
 
 @forum.route("/post/<int:post_id>/report", methods=["GET", "POST"])
 @forum.route("/post/<int:post_id>/report", methods=["GET", "POST"])
@@ -394,6 +393,13 @@ def report_post(post_id):
     return render_template("forum/report_post.html", form=form)
     return render_template("forum/report_post.html", form=form)
 
 
 
 
+@forum.route("/post/<int:post_id>/raw", methods=["POST", "GET"])
+@login_required
+def raw_post(post_id):
+    post = Post.query.filter_by(id=post_id).first_or_404()
+    return format_quote(post)
+
+
 @forum.route("/markread")
 @forum.route("/markread")
 @forum.route("/<int:forum_id>/markread")
 @forum.route("/<int:forum_id>/markread")
 @forum.route("/<int:forum_id>-<slug>/markread")
 @forum.route("/<int:forum_id>-<slug>/markread")
@@ -401,16 +407,17 @@ def report_post(post_id):
 def markread(forum_id=None, slug=None):
 def markread(forum_id=None, slug=None):
     # Mark a single forum as read
     # Mark a single forum as read
     if forum_id:
     if forum_id:
-        forum = Forum.query.filter_by(id=forum_id).first_or_404()
-        forumsread = ForumsRead.query.filter_by(user_id=current_user.id,
-                                                forum_id=forum.id).first()
+        forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
+        forumsread = ForumsRead.query.filter_by(
+            user_id=current_user.id, forum_id=forum_instance.id
+        ).first()
         TopicsRead.query.filter_by(user_id=current_user.id,
         TopicsRead.query.filter_by(user_id=current_user.id,
-                                   forum_id=forum.id).delete()
+                                   forum_id=forum_instance.id).delete()
 
 
         if not forumsread:
         if not forumsread:
             forumsread = ForumsRead()
             forumsread = ForumsRead()
             forumsread.user_id = current_user.id
             forumsread.user_id = current_user.id
-            forumsread.forum_id = forum.id
+            forumsread.forum_id = forum_instance.id
 
 
         forumsread.last_read = datetime.datetime.utcnow()
         forumsread.last_read = datetime.datetime.utcnow()
         forumsread.cleared = datetime.datetime.utcnow()
         forumsread.cleared = datetime.datetime.utcnow()
@@ -418,7 +425,7 @@ def markread(forum_id=None, slug=None):
         db.session.add(forumsread)
         db.session.add(forumsread)
         db.session.commit()
         db.session.commit()
 
 
-        return redirect(forum.url)
+        return redirect(forum_instance.url)
 
 
     # Mark all forums as read
     # Mark all forums as read
     ForumsRead.query.filter_by(user_id=current_user.id).delete()
     ForumsRead.query.filter_by(user_id=current_user.id).delete()
@@ -426,10 +433,10 @@ def markread(forum_id=None, slug=None):
 
 
     forums = Forum.query.all()
     forums = Forum.query.all()
     forumsread_list = []
     forumsread_list = []
-    for forum in forums:
+    for forum_instance in forums:
         forumsread = ForumsRead()
         forumsread = ForumsRead()
         forumsread.user_id = current_user.id
         forumsread.user_id = current_user.id
-        forumsread.forum_id = forum.id
+        forumsread.forum_id = forum_instance.id
         forumsread.last_read = datetime.datetime.utcnow()
         forumsread.last_read = datetime.datetime.utcnow()
         forumsread.cleared = datetime.datetime.utcnow()
         forumsread.cleared = datetime.datetime.utcnow()
         forumsread_list.append(forumsread)
         forumsread_list.append(forumsread)
@@ -477,7 +484,7 @@ def topictracker():
                   db.and_(TopicsRead.topic_id == Topic.id,
                   db.and_(TopicsRead.topic_id == Topic.id,
                           TopicsRead.user_id == current_user.id)).\
                           TopicsRead.user_id == current_user.id)).\
         add_entity(TopicsRead).\
         add_entity(TopicsRead).\
-        order_by(Post.id.desc()).\
+        order_by(Topic.last_updated.desc()).\
         paginate(page, flaskbb_config['TOPICS_PER_PAGE'], True)
         paginate(page, flaskbb_config['TOPICS_PER_PAGE'], True)
 
 
     return render_template("forum/topictracker.html", topics=topics)
     return render_template("forum/topictracker.html", topics=topics)

+ 65 - 47
flaskbb/management/forms.py

@@ -9,10 +9,10 @@
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
 from flask.ext.wtf import Form
 from flask.ext.wtf import Form
-from wtforms import (TextField, TextAreaField, PasswordField, IntegerField,
+from wtforms import (StringField, TextAreaField, PasswordField, IntegerField,
                      BooleanField, SelectField, DateField)
                      BooleanField, SelectField, DateField)
-from wtforms.validators import (Required, Optional, Email, regexp, Length, URL,
-                                ValidationError)
+from wtforms.validators import (DataRequired, Optional, Email, regexp, Length,
+                                URL, ValidationError)
 
 
 from wtforms.ext.sqlalchemy.fields import (QuerySelectField,
 from wtforms.ext.sqlalchemy.fields import (QuerySelectField,
                                            QuerySelectMultipleField)
                                            QuerySelectMultipleField)
@@ -36,16 +36,16 @@ def selectable_categories():
 
 
 
 
 def select_primary_group():
 def select_primary_group():
-    return Group.query.filter(Group.guest == False).order_by(Group.id)
+    return Group.query.filter(Group.guest != True).order_by(Group.id)
 
 
 
 
 class UserForm(Form):
 class UserForm(Form):
-    username = TextField("Username", validators=[
-        Required(),
+    username = StringField("Username", validators=[
+        DataRequired(message="A username is required."),
         is_username])
         is_username])
 
 
-    email = TextField("E-Mail", validators=[
-        Required(),
+    email = StringField("E-Mail", validators=[
+        DataRequired(message="A E-Mail address is required."),
         Email(message="This email is invalid")])
         Email(message="This email is invalid")])
 
 
     password = PasswordField("Password", validators=[
     password = PasswordField("Password", validators=[
@@ -60,13 +60,13 @@ class UserForm(Form):
         ("Male", "Male"),
         ("Male", "Male"),
         ("Female", "Female")])
         ("Female", "Female")])
 
 
-    location = TextField("Location", validators=[
+    location = StringField("Location", validators=[
         Optional()])
         Optional()])
 
 
-    website = TextField("Website", validators=[
+    website = StringField("Website", validators=[
         Optional(), URL()])
         Optional(), URL()])
 
 
-    avatar = TextField("Avatar", validators=[
+    avatar = StringField("Avatar", validators=[
         Optional(), URL()])
         Optional(), URL()])
 
 
     signature = TextAreaField("Forum Signature", validators=[
     signature = TextAreaField("Forum Signature", validators=[
@@ -85,15 +85,15 @@ class UserForm(Form):
         # TODO: Template rendering errors "NoneType is not callable"
         # TODO: Template rendering errors "NoneType is not callable"
         #       without this, figure out why.
         #       without this, figure out why.
         query_factory=select_primary_group,
         query_factory=select_primary_group,
-        allow_blank=True,
         get_label="name")
         get_label="name")
 
 
     def validate_username(self, field):
     def validate_username(self, field):
         if hasattr(self, "user"):
         if hasattr(self, "user"):
             user = User.query.filter(
             user = User.query.filter(
-                db.and_(User.username.like(field.data),
-                        db.not_(User.id == self.user.id)
-                        )
+                db.and_(
+                    User.username.like(field.data),
+                    db.not_(User.id == self.user.id)
+                )
             ).first()
             ).first()
         else:
         else:
             user = User.query.filter(User.username.like(field.data)).first()
             user = User.query.filter(User.username.like(field.data)).first()
@@ -104,9 +104,10 @@ class UserForm(Form):
     def validate_email(self, field):
     def validate_email(self, field):
         if hasattr(self, "user"):
         if hasattr(self, "user"):
             user = User.query.filter(
             user = User.query.filter(
-                db.and_(User.email.like(field.data),
-                        db.not_(User.id == self.user.id)
-                        )
+                db.and_(
+                    User.email.like(field.data),
+                    db.not_(User.id == self.user.id)
+                )
             ).first()
             ).first()
         else:
         else:
             user = User.query.filter(User.email.like(field.data)).first()
             user = User.query.filter(User.email.like(field.data)).first()
@@ -131,8 +132,8 @@ class EditUserForm(UserForm):
 
 
 
 
 class GroupForm(Form):
 class GroupForm(Form):
-    name = TextField("Group Name", validators=[
-        Required(message="Group name required")])
+    name = StringField("Group Name", validators=[
+        DataRequired(message="Group name required")])
 
 
     description = TextAreaField("Description", validators=[
     description = TextAreaField("Description", validators=[
         Optional()])
         Optional()])
@@ -194,9 +195,10 @@ class GroupForm(Form):
     def validate_name(self, field):
     def validate_name(self, field):
         if hasattr(self, "group"):
         if hasattr(self, "group"):
             group = Group.query.filter(
             group = Group.query.filter(
-                db.and_(Group.name.like(field.data),
-                        db.not_(Group.id == self.group.id)
-                        )
+                db.and_(
+                    Group.name.like(field.data),
+                    db.not_(Group.id == self.group.id)
+                )
             ).first()
             ).first()
         else:
         else:
             group = Group.query.filter(Group.name.like(field.data)).first()
             group = Group.query.filter(Group.name.like(field.data)).first()
@@ -207,9 +209,10 @@ class GroupForm(Form):
     def validate_banned(self, field):
     def validate_banned(self, field):
         if hasattr(self, "group"):
         if hasattr(self, "group"):
             group = Group.query.filter(
             group = Group.query.filter(
-                db.and_(Group.banned == True,
-                        db.not_(Group.id == self.group.id)
-                        )
+                db.and_(
+                    Group.banned,
+                    db.not_(Group.id == self.group.id)
+                )
             ).count()
             ).count()
         else:
         else:
             group = Group.query.filter_by(banned=True).count()
             group = Group.query.filter_by(banned=True).count()
@@ -220,9 +223,10 @@ class GroupForm(Form):
     def validate_guest(self, field):
     def validate_guest(self, field):
         if hasattr(self, "group"):
         if hasattr(self, "group"):
             group = Group.query.filter(
             group = Group.query.filter(
-                db.and_(Group.guest == True,
-                        db.not_(Group.id == self.group.id)
-                        )
+                db.and_(
+                    Group.guest,
+                    db.not_(Group.id == self.group.id)
+                )
             ).count()
             ).count()
         else:
         else:
             group = Group.query.filter_by(guest=True).count()
             group = Group.query.filter_by(guest=True).count()
@@ -247,15 +251,22 @@ class AddGroupForm(GroupForm):
 
 
 
 
 class ForumForm(Form):
 class ForumForm(Form):
-    title = TextField("Forum Title", validators=[
-        Required(message="Forum title required")])
+    title = StringField(
+        "Forum Title",
+        validators=[DataRequired(message="Forum title required")]
+    )
 
 
-    description = TextAreaField("Description", validators=[
-        Optional()],
-        description="You can format your description with BBCode.")
+    description = TextAreaField(
+        "Description",
+        validators=[Optional()],
+        description="You can format your description with BBCode."
+    )
 
 
-    position = IntegerField("Position", default=1, validators=[
-        Required(message="Forum position required")])
+    position = IntegerField(
+        "Position",
+        default=1,
+        validators=[DataRequired(message="Forum position required")]
+    )
 
 
     category = QuerySelectField(
     category = QuerySelectField(
         "Category",
         "Category",
@@ -265,11 +276,13 @@ class ForumForm(Form):
         description="The category that contains this forum."
         description="The category that contains this forum."
     )
     )
 
 
-    external = TextField("External link", validators=[
-        Optional(), URL()],
-        description="A link to a website i.e. 'http://flaskbb.org'")
+    external = StringField(
+        "External link",
+        validators=[Optional(), URL()],
+        description="A link to a website i.e. 'http://flaskbb.org'"
+    )
 
 
-    moderators = TextField(
+    moderators = StringField(
         "Moderators",
         "Moderators",
         description="Comma seperated usernames. Leave it blank if you do not \
         description="Comma seperated usernames. Leave it blank if you do not \
                      want to set any moderators."
                      want to set any moderators."
@@ -352,15 +365,20 @@ class AddForumForm(ForumForm):
 
 
 
 
 class CategoryForm(Form):
 class CategoryForm(Form):
-    title = TextField("Category title", validators=[
-        Required(message="Category title required")])
+    title = StringField("Category title", validators=[
+        DataRequired(message="Category title required")])
 
 
-    description = TextAreaField("Description", validators=[
-        Optional()],
-        description="You can format your description with BBCode.")
+    description = TextAreaField(
+        "Description",
+        validators=[Optional()],
+        description="You can format your description with BBCode."
+    )
 
 
-    position = IntegerField("Position", default=1, validators=[
-        Required(message="Category position required")])
+    position = IntegerField(
+        "Position",
+        default=1,
+        validators=[DataRequired(message="Category position required")]
+    )
 
 
     def save(self):
     def save(self):
         category = Category(**self.data)
         category = Category(**self.data)

+ 21 - 28
flaskbb/management/models.py

@@ -12,6 +12,7 @@ import sys
 from wtforms import (TextField, IntegerField, FloatField, BooleanField,
 from wtforms import (TextField, IntegerField, FloatField, BooleanField,
                      SelectField, SelectMultipleField, validators)
                      SelectField, SelectMultipleField, validators)
 from flask.ext.wtf import Form
 from flask.ext.wtf import Form
+from flaskbb._compat import max_integer, text_type, iteritems
 from flaskbb.extensions import db, cache
 from flaskbb.extensions import db, cache
 
 
 
 
@@ -74,32 +75,23 @@ class Setting(db.Model):
         for setting in group.settings:
         for setting in group.settings:
             field_validators = []
             field_validators = []
 
 
+            if setting.value_type in ("integer", "float"):
+                validator_class = validators.NumberRange
+            elif setting.value_type == "string":
+                validator_class = validators.Length
+
             # generate the validators
             # generate the validators
             if "min" in setting.extra:
             if "min" in setting.extra:
                 # Min number validator
                 # Min number validator
-                if setting.value_type in ("integer", "float"):
-                    field_validators.append(
-                        validators.NumberRange(min=setting.extra["min"])
-                    )
-
-                # Min text length validator
-                elif setting.value_type in ("string"):
-                    field_validators.append(
-                        validators.Length(min=setting.extra["min"])
-                    )
+                field_validators.append(
+                    validator_class(min=setting.extra["min"])
+                )
 
 
             if "max" in setting.extra:
             if "max" in setting.extra:
                 # Max number validator
                 # Max number validator
-                if setting.value_type in ("integer", "float"):
-                    field_validators.append(
-                        validators.NumberRange(max=setting.extra["max"])
-                    )
-
-                # Max text length validator
-                elif setting.value_type in ("string"):
-                    field_validators.append(
-                        validators.Length(max=setting.extra["max"])
-                    )
+                field_validators.append(
+                    validator_class(max=setting.extra["max"])
+                )
 
 
             # Generate the fields based on value_type
             # Generate the fields based on value_type
             # IntegerField
             # IntegerField
@@ -118,7 +110,7 @@ class Setting(db.Model):
                 )
                 )
 
 
             # TextField
             # TextField
-            if setting.value_type == "string":
+            elif setting.value_type == "string":
                 setattr(
                 setattr(
                     SettingsForm, setting.key,
                     SettingsForm, setting.key,
                     TextField(setting.name, validators=field_validators,
                     TextField(setting.name, validators=field_validators,
@@ -126,12 +118,12 @@ class Setting(db.Model):
                 )
                 )
 
 
             # SelectMultipleField
             # SelectMultipleField
-            if setting.value_type == "selectmultiple":
+            elif setting.value_type == "selectmultiple":
                 # if no coerce is found, it will fallback to unicode
                 # if no coerce is found, it will fallback to unicode
                 if "coerce" in setting.extra:
                 if "coerce" in setting.extra:
                     coerce_to = setting.extra['coerce']
                     coerce_to = setting.extra['coerce']
                 else:
                 else:
-                    coerce_to = unicode
+                    coerce_to = text_type
 
 
                 setattr(
                 setattr(
                     SettingsForm, setting.key,
                     SettingsForm, setting.key,
@@ -144,12 +136,12 @@ class Setting(db.Model):
                 )
                 )
 
 
             # SelectField
             # SelectField
-            if setting.value_type == "select":
+            elif setting.value_type == "select":
                 # if no coerce is found, it will fallback to unicode
                 # if no coerce is found, it will fallback to unicode
                 if "coerce" in setting.extra:
                 if "coerce" in setting.extra:
                     coerce_to = setting.extra['coerce']
                     coerce_to = setting.extra['coerce']
                 else:
                 else:
-                    coerce_to = unicode
+                    coerce_to = text_type
 
 
                 setattr(
                 setattr(
                     SettingsForm, setting.key,
                     SettingsForm, setting.key,
@@ -161,7 +153,7 @@ class Setting(db.Model):
                 )
                 )
 
 
             # BooleanField
             # BooleanField
-            if setting.value_type == "boolean":
+            elif setting.value_type == "boolean":
                 setattr(
                 setattr(
                     SettingsForm, setting.key,
                     SettingsForm, setting.key,
                     BooleanField(setting.name, description=setting.description)
                     BooleanField(setting.name, description=setting.description)
@@ -181,7 +173,7 @@ class Setting(db.Model):
         :param settings: A dictionary with setting items.
         :param settings: A dictionary with setting items.
         """
         """
         # update the database
         # update the database
-        for key, value in settings.iteritems():
+        for key, value in iteritems(settings):
             setting = cls.query.filter(Setting.key == key.lower()).first()
             setting = cls.query.filter(Setting.key == key.lower()).first()
 
 
             setting.value = value
             setting.value = value
@@ -218,7 +210,7 @@ class Setting(db.Model):
         return settings
         return settings
 
 
     @classmethod
     @classmethod
-    @cache.memoize(timeout=sys.maxint)
+    @cache.memoize(timeout=max_integer)
     def as_dict(cls, from_group=None, upper=True):
     def as_dict(cls, from_group=None, upper=True):
         """Returns all settings as a dict. This method is cached. If you want
         """Returns all settings as a dict. This method is cached. If you want
         to invalidate the cache, simply execute ``self.invalidate_cache()``.
         to invalidate the cache, simply execute ``self.invalidate_cache()``.
@@ -235,6 +227,7 @@ class Setting(db.Model):
                 first_or_404()
                 first_or_404()
             result = result.settings
             result = result.settings
         else:
         else:
+            print(Setting.query)
             result = cls.query.all()
             result = cls.query.all()
 
 
         for setting in result:
         for setting in result:

+ 14 - 10
flaskbb/management/views.py

@@ -18,13 +18,14 @@ from flask.ext.login import current_user
 from flask.ext.plugins import get_all_plugins, get_plugin, get_plugin_from_all
 from flask.ext.plugins import get_all_plugins, get_plugin, get_plugin_from_all
 
 
 from flaskbb import __version__ as flaskbb_version
 from flaskbb import __version__ as flaskbb_version
+from flaskbb._compat import iteritems
 from flaskbb.forum.forms import UserSearchForm
 from flaskbb.forum.forms import UserSearchForm
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.helpers import render_template
 from flaskbb.utils.helpers import render_template
 from flaskbb.utils.decorators import admin_required, moderator_required
 from flaskbb.utils.decorators import admin_required, moderator_required
 from flaskbb.utils.permissions import can_ban_user, can_edit_user
 from flaskbb.utils.permissions import can_ban_user, can_edit_user
 from flaskbb.extensions import db
 from flaskbb.extensions import db
-from flaskbb.user.models import User, Group
+from flaskbb.user.models import Guest, User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
 from flaskbb.forum.models import Post, Topic, Forum, Category, Report
 from flaskbb.management.models import Setting, SettingsGroup
 from flaskbb.management.models import Setting, SettingsGroup
 from flaskbb.management.forms import (AddUserForm, EditUserForm, AddGroupForm,
 from flaskbb.management.forms import (AddUserForm, EditUserForm, AddGroupForm,
@@ -70,7 +71,7 @@ def settings(slug=None):
     form = SettingsForm()
     form = SettingsForm()
 
 
     if form.validate_on_submit():
     if form.validate_on_submit():
-        for key, values in old_settings.iteritems():
+        for key, values in iteritems(old_settings):
             try:
             try:
                 # check if the value has changed
                 # check if the value has changed
                 if values['value'] == form[key].data:
                 if values['value'] == form[key].data:
@@ -82,7 +83,7 @@ def settings(slug=None):
 
 
         Setting.update(settings=new_settings, app=current_app)
         Setting.update(settings=new_settings, app=current_app)
     else:
     else:
-        for key, values in old_settings.iteritems():
+        for key, values in iteritems(old_settings):
             try:
             try:
                 form[key].data = values['value']
                 form[key].data = values['value']
             except (KeyError, ValueError):
             except (KeyError, ValueError):
@@ -124,7 +125,7 @@ def edit_user(user_id):
 
 
     secondary_group_query = Group.query.filter(
     secondary_group_query = Group.query.filter(
         db.not_(Group.id == user.primary_group_id),
         db.not_(Group.id == user.primary_group_id),
-        db.not_(Group.banned == True),
+        db.not_(Group.banned),
         db.not_(Group.guest == True))
         db.not_(Group.guest == True))
 
 
     form = EditUserForm(user)
     form = EditUserForm(user)
@@ -179,7 +180,6 @@ def banned_users():
         Group.id == User.primary_group_id
         Group.id == User.primary_group_id
     ).paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
     ).paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
 
 
-
     if search_form.validate():
     if search_form.validate():
         users = search_form.get_results().\
         users = search_form.get_results().\
             paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
             paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
@@ -203,10 +203,10 @@ def ban_user(user_id):
     # Do not allow moderators to ban admins
     # Do not allow moderators to ban admins
     if user.get_permissions()['admin'] and \
     if user.get_permissions()['admin'] and \
             (current_user.permissions['mod'] or
             (current_user.permissions['mod'] or
-                current_user.permissions['super_mod']):
+             current_user.permissions['super_mod']):
 
 
-            flash("A moderator cannot ban an admin user.", "danger")
-            return redirect(url_for("management.overview"))
+        flash("A moderator cannot ban an admin user.", "danger")
+        return redirect(url_for("management.overview"))
 
 
     if user.ban():
     if user.ban():
         flash("User was banned successfully.", "success")
         flash("User was banned successfully.", "success")
@@ -314,6 +314,9 @@ def edit_group(group_id):
         form.populate_obj(group)
         form.populate_obj(group)
         group.save()
         group.save()
 
 
+        if group.guest:
+            Guest.invalidate_cache()
+
         flash("Group successfully edited.", "success")
         flash("Group successfully edited.", "success")
         return redirect(url_for("management.groups", group_id=group.id))
         return redirect(url_for("management.groups", group_id=group.id))
 
 
@@ -365,8 +368,9 @@ def edit_forum(forum_id):
         return redirect(url_for("management.edit_forum", forum_id=forum.id))
         return redirect(url_for("management.edit_forum", forum_id=forum.id))
     else:
     else:
         if forum.moderators:
         if forum.moderators:
-            form.moderators.data = ",".join([user.username
-                                            for user in forum.moderators])
+            form.moderators.data = ",".join([
+                user.username for user in forum.moderators
+            ])
         else:
         else:
             form.moderators.data = None
             form.moderators.data = None
 
 

+ 113 - 115
flaskbb/plugins/portal/templates/index.html

@@ -76,132 +76,130 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block content %}
 {% block content %}
-  <div class="container main-content">
-    <div class="row">
-
-      <!-- Left -->
-      <div class="col-md-8">
-        <div class="panel panel-default">
-          <div class="panel-heading">
-            <h3 class="panel-title">News</h3>
-          </div>
-          <div class="panel-body" style="padding-top: 0px">
-
-          {% for topic in news.items %}
-            <h1><a href="{{ topic.url }}">{{ topic.title }}</a></h1>
-            <ul class="portal-info">
-                <li><i class="fa fa-calendar"></i> {{ topic.date_created|format_date('%b %d %Y') }}</li>
-                <li><i class="fa fa-user"></i> <a href="{{ url_for('user.profile', username=topic.user.username) }}">{{ topic.user.username }}</a></li>
-                <li><i class="fa fa-comment"></i> <a href="{{ topic.url }}">Comments ({{ topic.post_count }})</a></li>
-            </ul>
-            <div class="portal-content">
-                {{ topic.first_post.content | markup | safe }}<br />
-            </div>
-            {% if not loop.last %}<hr>{% endif %}
-          {% endfor %}
+<div class="row">
 
 
-          </div> <!-- /.panel-body -->
+  <!-- Left -->
+  <div class="col-md-8">
+    <div class="panel panel-default panel-widget">
+      <div class="panel-heading panel-widget-heading">
+        <h3 class="panel-title">News</h3>
+      </div>
+      <div class="panel-body panel-widget-body" style="padding-top: 0px">
+
+      {% for topic in news.items %}
+        <h1><a href="{{ topic.url }}">{{ topic.title }}</a></h1>
+        <ul class="portal-info">
+            <li><i class="fa fa-calendar"></i> {{ topic.date_created|format_date('%b %d %Y') }}</li>
+            <li><i class="fa fa-user"></i> <a href="{{ url_for('user.profile', username=topic.user.username) }}">{{ topic.user.username }}</a></li>
+            <li><i class="fa fa-comment"></i> <a href="{{ topic.url }}">Comments ({{ topic.post_count }})</a></li>
+        </ul>
+        <div class="portal-content">
+            {{ topic.first_post.content | markup | safe }}<br />
         </div>
         </div>
+        {% if not loop.last %}<hr>{% endif %}
+      {% endfor %}
+
+      </div> <!-- /.panel-body -->
+    </div>
 
 
+  </div>
+
+  <!-- Right -->
+  <div class="col-md-4">
+    <div class="panel panel-default panel-widget">
+      <div class="panel-heading panel-widget-heading">
+        <h3 class="panel-title">Recent Topics</h3>
       </div>
       </div>
+      <div class="panel-body panel-widget-body">
+      {% for topic in recent_topics %}
 
 
-      <!-- Right -->
-      <div class="col-md-4">
-        <div class="panel panel-default">
-          <div class="panel-heading">
-            <h3 class="panel-title">Recent Topics</h3>
-          </div>
-          <div class="panel-body">
-          {% for topic in recent_topics %}
-
-              <div class="portal-topic">
-                <div class="portal-topic-name">
-                  <a href="{{ topic.url }}">{{ topic.title }}</a>
-                </div>
-                <div class="portal-topic-updated-by">
-                  <a href="{{ url_for('user.profile', username=topic.user.username) }}">{{ topic.user.username }}</a>
-                </div>
-                <div class="portal-topic-updated">
-                  {{ topic.date_created | time_since }}
-                </div>
-              </div> <!-- /.topic -->
-
-          {% endfor %}
-          </div>
-        </div>
+          <div class="portal-topic">
+            <div class="portal-topic-name">
+              <a href="{{ topic.url }}">{{ topic.title | truncate(length=45) }}</a>
+            </div>
+            <div class="portal-topic-updated-by">
+              <a href="{{ url_for('user.profile', username=topic.user.username) }}">{{ topic.user.username }}</a>
+            </div>
+            <div class="portal-topic-updated">
+              {{ topic.date_created | time_since }}
+            </div>
+          </div> <!-- /.topic -->
 
 
-        <div class="panel panel-default">
-          <div class="panel-heading">
-            <h3 class="panel-title">Statistics</h3>
-          </div>
-          <div class="panel-body">
-
-                <div class="portal-stats">
-                  <div class="portal-stats-left">
-                    Topics
-                  </div>
-                  <div class="portal-stats-right">
-                    {{ topic_count }}
-                  </div>
-                </div>
-
-                <div class="portal-stats">
-                  <div class="portal-stats-left">
-                    Posts
-                  </div>
-                  <div class="portal-stats-right">
-                    {{ post_count }}
-                  </div>
-                </div>
-
-                <div class="portal-stats">
-                  <div class="portal-stats-left">
-                    Registered Users
-                  </div>
-                  <div class="portal-stats-right">
-                    {{ user_count }}
-                  </div>
-                </div>
-
-                {% if newest_user %}
-                <div class="portal-stats">
-                  <div class="portal-stats-left">
-                    Newest User
-                  </div>
-                  <div class="portal-stats-right">
-                    <a href="{{ newest_user.url }}">{{ newest_user.username }}</a>
-                  </div>
-                </div>
-                {% endif %}
-
-                <div class="portal-stats">
-                  <div class="portal-stats-left">
-                    Online Users
-                  </div>
-
-                  <div class="portal-stats-right">
-                    {{ online_users }}
-                  </div>
-                </div>
-
-                {% if config["REDIS_ENABLED"] %}
-                <div class="portal-stats">
-                  <div class="portal-stats-left">
-                    Guests online
-                  </div>
-
-                  <div class="portal-stats-right">
-                    {{ online_guests }}
-                  </div>
-                </div>
-                {% endif %}
-          </div>
-        </div>
+      {% endfor %}
+      </div>
+    </div>
+
+    <div class="panel panel-default panel-widget">
+      <div class="panel-heading panel-widget-heading">
+        <h3 class="panel-title">Statistics</h3>
       </div>
       </div>
+      <div class="panel-body panel-widget-body">
+
+            <div class="portal-stats">
+              <div class="portal-stats-left">
+                Topics
+              </div>
+              <div class="portal-stats-right">
+                {{ topic_count }}
+              </div>
+            </div>
+
+            <div class="portal-stats">
+              <div class="portal-stats-left">
+                Posts
+              </div>
+              <div class="portal-stats-right">
+                {{ post_count }}
+              </div>
+            </div>
+
+            <div class="portal-stats">
+              <div class="portal-stats-left">
+                Registered Users
+              </div>
+              <div class="portal-stats-right">
+                {{ user_count }}
+              </div>
+            </div>
+
+            {% if newest_user %}
+            <div class="portal-stats">
+              <div class="portal-stats-left">
+                Newest User
+              </div>
+              <div class="portal-stats-right">
+                <a href="{{ newest_user.url }}">{{ newest_user.username }}</a>
+              </div>
+            </div>
+            {% endif %}
 
 
+            <div class="portal-stats">
+              <div class="portal-stats-left">
+                Online Users
+              </div>
+
+              <div class="portal-stats-right">
+                {{ online_users }}
+              </div>
+            </div>
+
+            {% if config["REDIS_ENABLED"] %}
+            <div class="portal-stats">
+              <div class="portal-stats-left">
+                Guests online
+              </div>
+
+              <div class="portal-stats-right">
+                {{ online_guests }}
+              </div>
+            </div>
+            {% endif %}
+      </div>
     </div>
     </div>
   </div>
   </div>
 
 
+</div>
+
 
 
 
 
 {% endblock %}
 {% endblock %}

+ 1 - 1
flaskbb/plugins/portal/views.py

@@ -28,7 +28,7 @@ def index():
         order_by(Topic.id.desc()).\
         order_by(Topic.id.desc()).\
         paginate(page, flaskbb_config["TOPICS_PER_PAGE"], True)
         paginate(page, flaskbb_config["TOPICS_PER_PAGE"], True)
 
 
-    recent_topics = Topic.query.order_by(Topic.date_created).limit(5).offset(0)
+    recent_topics = Topic.query.order_by(Topic.last_updated.desc()).limit(5)
 
 
     user_count = User.query.count()
     user_count = User.query.count()
     topic_count = Topic.query.count()
     topic_count = Topic.query.count()

+ 12 - 16
flaskbb/static/js/topic.js

@@ -1,22 +1,18 @@
 /**
 /**
- * Topic.js
+ * topic.js
  */
  */
 $(document).ready(function () {
 $(document).ready(function () {
-        $(".quote_btn").click(function (event) {
-            event.preventDefault();
+    // Quote
+    $('.quote_btn').click(function (event) {
+        event.preventDefault();
+        var post_id = $(this).attr('data-post-id');
 
 
-            // QuickReply Textarea
-            var $contents = $(".reply-content textarea#content");
-            // Original Post
-            var $original = $(".post_body#" + $(this).attr('data-post-id'));
-            // Content of the Post, in plaintext (strips tags) and without the signature
-            var content = $original.clone().find('.signature').remove().end().text().trim();
-
-            // Add quote to the Quickreply Textarea
-            if ($contents.length > 0) {
-                $contents.val($contents.val() + "\n[quote]" + content + "[/quote]");
-            } else {
-        $contents.val("[quote]" + content + "[/quote]");
-        }
+        $.get('/post/' + post_id + '/raw', function(text) {
+            var $contents = $('.reply-content textarea#content');
+            $contents.val(($contents.val() + '\n' + text).trim() + '\n');
+            $contents.selectionStart = $contents.selectionEnd = $contents.val().length;
+            $contents[0].scrollTop = $contents[0].scrollHeight;
+            window.location.href = '#content';
+        });
     });
     });
 });
 });

+ 6 - 6
flaskbb/templates/forum/category_layout.html

@@ -71,16 +71,16 @@
 
 
             <td valign="top" align="right" style="white-space: nowrap">
             <td valign="top" align="right" style="white-space: nowrap">
                 {% if forum.last_post_id %}
                 {% if forum.last_post_id %}
-                <a href="{{ forum.last_post.url }}" title="{{ forum.last_post.topic.title }}">
-                    <strong>{{ forum.last_post.topic.title|crop_title }}</strong>
+                <a href="{{ forum.last_post_url }}" title="{{ forum.last_post_title }}">
+                    <strong>{{ forum.last_post_title|crop_title }}</strong>
                 </a>
                 </a>
                 <br />
                 <br />
-                {{ forum.last_post.date_created|time_since }}<br />
+                {{ forum.last_post_created|time_since }}<br />
 
 
-                    {% if forum.last_post.user_id %}
-                    by <a href="{{ url_for('user.profile', username=forum.last_post.user.username) }}">{{ forum.last_post.user.username }}</a>
+                    {% if forum.last_post_user_id %}
+                    by <a href="{{ url_for('user.profile', username=forum.last_post_username) }}">{{ forum.last_post_username }}</a>
                     {% else %}
                     {% else %}
-                    {{ forum.last_post.username }}
+                    {{ forum.last_post_username }}
                     {% endif %}
                     {% endif %}
 
 
                 {% else %}
                 {% else %}

+ 22 - 63
flaskbb/templates/forum/topic.html

@@ -12,54 +12,11 @@
     <li class="active">{{ topic.title }}</li>
     <li class="active">{{ topic.title }}</li>
 </ol>
 </ol>
 
 
-<div class="pull-left" style="padding-bottom: 10px">
-    {{ render_pagination(posts, topic.url) }}
-</div> <!-- end span pagination -->
-
-<div class="pull-right" style="padding-bottom: 10px">
-    <div class="btn btn-group">
-    {% if current_user|delete_topic(topic.first_post.user_id, topic.forum) %}
-        <a href="{{ url_for('forum.delete_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-danger">
-            <span class="fa fa-trash-o"></span> Delete Topic
-        </a>
-    {% endif %}
-    {% if current_user|can_moderate(topic.forum) %}
-        {% if not topic.locked %}
-            <a href="{{ url_for('forum.lock_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-warning">
-                <span class="fa fa-lock"></span> Lock Topic
-            </a>
-        {% else %}
-            <a href="{{ url_for('forum.unlock_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-warning">
-                <span class="fa fa-unlock"></span> Unlock Topic
-            </a>
-        {% endif %}
-    {% endif %}
-    </div>
-
-    {% if current_user.is_authenticated() %}
-    <div class="btn btn-group">
-        {% if current_user.is_tracking_topic(topic) %}
-        <a href="{{ url_for('forum.untrack_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-default"><span class="fa fa-star">
-            </span> Untrack Topic
-        </a>
-        {% else %}
-        <a href="{{ url_for('forum.track_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-default">
-            <span class="fa fa-star"></span> Track Topic
-        </a>
-        {% endif %}
-
-        {% if current_user|post_reply(topic.forum) and not (topic.locked or topic.forum.locked) %}
-        <a href="{{ url_for('forum.new_post', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-primary">
-            <span class="fa fa-pencil"></span> Reply
-        </a>
-        {% endif %}
-    </div>
-    {% endif %}
-</div>
+{% include 'forum/topic_controls.html' %}
 
 
 <table class="table table-bordered">
 <table class="table table-bordered">
     <tbody>
     <tbody>
-        {% for post in posts.items %}
+        {% for post, user in posts.items %}
         <tr>
         <tr>
             <td >
             <td >
                 <span class="pull-right">
                 <span class="pull-right">
@@ -90,28 +47,28 @@
             <table class="table table-borderless">
             <table class="table table-borderless">
                 <tr>
                 <tr>
                 {% if post.user_id %}
                 {% if post.user_id %}
-                    {% if post.user.avatar %}
+                    {% if user.avatar %}
                     <td width="1">
                     <td width="1">
-                        <img src="{{ post.user.avatar }}" alt="Avatar" height="100" width="100">
+                        <img src="{{ user.avatar }}" alt="Avatar" height="100" width="100">
                     </td>
                     </td>
                     {% endif %}
                     {% endif %}
                     <td>
                     <td>
-                        <a href="{{ post.user.url }}">
-                            <span style="font-weight:bold">{{ post.user.username }}</span> <!-- TODO: Implement userstyles -->
+                        <a href="{{ user.url }}">
+                            <span style="font-weight:bold">{{ user.username }}</span> <!-- TODO: Implement userstyles -->
                         </a>
                         </a>
-                            {%- if post.user|is_online %}
+                            {%- if user|is_online %}
                             <span class="label label-success">Online</span>
                             <span class="label label-success">Online</span>
                             {%- else %}
                             {%- else %}
                             <span class="label label-default">Offline</span>
                             <span class="label label-default">Offline</span>
                             {%- endif %}
                             {%- endif %}
                             <div class="profile primary-group">
                             <div class="profile primary-group">
-                            {{ post.user.primary_group.name }}
+                            {{ user.primary_group.name }}
                             </div>
                             </div>
                     </td>
                     </td>
 
 
                     <td class="pull-right">
                     <td class="pull-right">
-                        Posts: {{ post.user.post_count }}<br />
-                        Registered since: {{ post.user.date_joined|format_date('%b %d %Y') }}<br />
+                        Posts: {{ user.post_count }}<br />
+                        Registered since: {{ user.date_joined|format_date('%b %d %Y') }}<br />
                     </td>
                     </td>
                 {% else %}
                 {% else %}
                     <td>
                     <td>
@@ -131,10 +88,10 @@
                 {% autoescape false %}
                 {% autoescape false %}
                     {{ post.content|markup }}
                     {{ post.content|markup }}
                     <!-- Signature Begin -->
                     <!-- Signature Begin -->
-                    {% if post.user_id and post.user.signature %}
+                    {% if post.user_id and user.signature %}
                     <div class="signature">
                     <div class="signature">
                         <hr>
                         <hr>
-                        {{ post.user.signature|markup }}
+                        {{ user.signature|markup }}
                     </div>
                     </div>
                     {% endif %}
                     {% endif %}
                     <!-- Signature End -->
                     <!-- Signature End -->
@@ -148,8 +105,8 @@
                     {% if current_user.is_authenticated() and post.user_id and post.user_id != current_user.id %}
                     {% if current_user.is_authenticated() and post.user_id and post.user_id != current_user.id %}
                     <a href="{{ url_for('user.new_message', to_user=post.user.username) }}">PM</a>
                     <a href="{{ url_for('user.new_message', to_user=post.user.username) }}">PM</a>
                     {% endif %}
                     {% endif %}
-                    {% if post.user.website %}
-                    | <a href="{{post.user.website}}">Website</a>
+                    {% if user.website %}
+                    {% if current_user.is_authenticated() %}| {% endif %}<a href="{{ user.website }}">Website</a>
                     {% endif %}
                     {% endif %}
                 </span>
                 </span>
 
 
@@ -159,21 +116,21 @@
                         Report
                         Report
                     </a> |
                     </a> |
                     {% endif %}
                     {% endif %}
-                    {% if current_user|edit_post(post.user_id, topic.forum) %}
+                    {% if current_user|edit_post(post) %}
                     <a href="{{ url_for('forum.edit_post', post_id=post.id) }}">Edit</a> |
                     <a href="{{ url_for('forum.edit_post', post_id=post.id) }}">Edit</a> |
                     {% endif %}
                     {% endif %}
                     {% if topic.first_post_id == post.id %}
                     {% if topic.first_post_id == post.id %}
-                        {% if current_user|delete_topic(topic.first_post.user_id, topic.forum) %}
+                        {% if current_user|delete_topic(topic) %}
                         <a href="{{ url_for('forum.delete_topic', topic_id=topic.id, slug=topic.slug) }}">Delete</a> |
                         <a href="{{ url_for('forum.delete_topic', topic_id=topic.id, slug=topic.slug) }}">Delete</a> |
                         {% endif %}
                         {% endif %}
                     {% else %}
                     {% else %}
-                        {% if current_user|delete_post(post.user_id, topic.forum) %}
+                        {% if current_user|delete_post(post) %}
                         <a href="{{ url_for('forum.delete_post', post_id=post.id) }}">Delete</a> |
                         <a href="{{ url_for('forum.delete_post', post_id=post.id) }}">Delete</a> |
                         {% endif %}
                         {% endif %}
                     {% endif %}
                     {% endif %}
-                    {% if current_user|post_reply(topic.forum) and not (topic.locked or topic.forum.locked) %}
+                    {% if current_user|post_reply(topic) %}
                         <!-- Quick quote -->
                         <!-- Quick quote -->
-                        <a href="#" class="quote_btn" data-post-id="pid{{ post.id }}">Quote</a> |
+                        <a href="#" class="quote_btn" data-post-id="{{ post.id }}">Quote</a> |
                         <!-- Full quote/reply -->
                         <!-- Full quote/reply -->
                         <a href="{{ url_for('forum.reply_post', topic_id=topic.id, post_id=post.id) }}">Reply</a>
                         <a href="{{ url_for('forum.reply_post', topic_id=topic.id, post_id=post.id) }}">Reply</a>
                     {% endif %}
                     {% endif %}
@@ -184,6 +141,8 @@
     </tbody>
     </tbody>
 </table>
 </table>
 
 
+{% include 'forum/topic_controls.html' %}
+
 {% if form %}
 {% if form %}
     {% from "macros.html" import render_field %}
     {% from "macros.html" import render_field %}
     <form class="form" action="#" method="post">
     <form class="form" action="#" method="post">
@@ -198,5 +157,5 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block scripts %}
 {% block scripts %}
-<script type="text/javascript" src="{{ url_for('static', filename='js/topic.js') }}"></script>
+    <script type="text/javascript" src="{{ url_for('static', filename='js/topic.js') }}"></script>
 {% endblock %}
 {% endblock %}

+ 46 - 0
flaskbb/templates/forum/topic_controls.html

@@ -0,0 +1,46 @@
+<div class="pull-left" style="padding-bottom: 10px">
+    {{ render_pagination(posts, topic.url) }}
+</div> <!-- end span pagination -->
+
+<div class="pull-right" style="padding-bottom: 10px">
+    <div class="btn btn-group">
+        {% if current_user|delete_topic(topic) %}
+        <a href="{{ url_for('forum.delete_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-danger">
+            <span class="fa fa-trash-o"></span> Delete Topic
+        </a>
+        {% endif %}
+        {% if current_user|can_moderate(topic.forum) %}
+        {% if not topic.locked %}
+        <a href="{{ url_for('forum.lock_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-warning">
+            <span class="fa fa-lock"></span> Lock Topic
+        </a>
+        {% else %}
+        <a href="{{ url_for('forum.unlock_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-warning">
+            <span class="fa fa-unlock"></span> Unlock Topic
+        </a>
+        {% endif %}
+        {% endif %}
+    </div>
+
+    {% if current_user.is_authenticated() %}
+    <div class="btn btn-group">
+        {% if current_user.is_tracking_topic(topic) %}
+        <a href="{{ url_for('forum.untrack_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-default"><span class="fa fa-star">
+            </span> Untrack Topic
+        </a>
+        {% else %}
+        <a href="{{ url_for('forum.track_topic', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-default">
+            <span class="fa fa-star"></span> Track Topic
+        </a>
+        {% endif %}
+
+        {% if current_user|post_reply(topic) %}
+        <a href="{{ url_for('forum.new_post', topic_id=topic.id, slug=topic.slug) }}" class="btn btn-primary">
+            <span class="fa fa-pencil"></span> Reply
+        </a>
+        {% endif %}
+    </div>
+    {% endif %}
+</div>
+
+<div class="clearfix"></div>

+ 19 - 19
flaskbb/user/forms.py

@@ -10,9 +10,9 @@
 """
 """
 from flask.ext.login import current_user
 from flask.ext.login import current_user
 from flask.ext.wtf import Form
 from flask.ext.wtf import Form
-from wtforms import (TextField, PasswordField, DateField, TextAreaField,
+from wtforms import (StringField, PasswordField, DateField, TextAreaField,
                      SelectField, ValidationError)
                      SelectField, ValidationError)
-from wtforms.validators import (Length, Required, Email, EqualTo, regexp,
+from wtforms.validators import (Length, DataRequired, Email, EqualTo, regexp,
                                 Optional, URL)
                                 Optional, URL)
 
 
 from flaskbb.user.models import User, PrivateMessage
 from flaskbb.user.models import User, PrivateMessage
@@ -34,16 +34,16 @@ class GeneralSettingsForm(Form):
 
 
 
 
 class ChangeEmailForm(Form):
 class ChangeEmailForm(Form):
-    old_email = TextField("Old E-Mail Address", validators=[
-        Required(message="Email adress required"),
+    old_email = StringField("Old E-Mail Address", validators=[
+        DataRequired(message="Email address required"),
         Email(message="This email is invalid")])
         Email(message="This email is invalid")])
 
 
-    new_email = TextField("New E-Mail Address", validators=[
-        Required(message="Email adress required"),
+    new_email = StringField("New E-Mail Address", validators=[
+        DataRequired(message="Email address required"),
         Email(message="This email is invalid")])
         Email(message="This email is invalid")])
 
 
-    confirm_new_email = TextField("Confirm E-Mail Address", validators=[
-        Required(message="Email adress required"),
+    confirm_new_email = StringField("Confirm E-Mail Address", validators=[
+        DataRequired(message="Email adress required"),
         Email(message="This email is invalid"),
         Email(message="This email is invalid"),
         EqualTo("new_email", message="E-Mails do not match")])
         EqualTo("new_email", message="E-Mails do not match")])
 
 
@@ -62,13 +62,13 @@ class ChangeEmailForm(Form):
 
 
 class ChangePasswordForm(Form):
 class ChangePasswordForm(Form):
     old_password = PasswordField("Old Password", validators=[
     old_password = PasswordField("Old Password", validators=[
-        Required(message="Password required")])
+        DataRequired(message="Password required")])
 
 
     new_password = PasswordField("New Password", validators=[
     new_password = PasswordField("New Password", validators=[
-        Required(message="Password required")])
+        DataRequired(message="Password required")])
 
 
     confirm_new_password = PasswordField("Confirm New Password", validators=[
     confirm_new_password = PasswordField("Confirm New Password", validators=[
-        Required(message="Password required"),
+        DataRequired(message="Password required"),
         EqualTo("new_password", message="Passwords do not match")])
         EqualTo("new_password", message="Passwords do not match")])
 
 
 
 
@@ -82,13 +82,13 @@ class ChangeUserDetailsForm(Form):
         ("Male", "Male"),
         ("Male", "Male"),
         ("Female", "Female")])
         ("Female", "Female")])
 
 
-    location = TextField("Location", validators=[
+    location = StringField("Location", validators=[
         Optional()])
         Optional()])
 
 
-    website = TextField("Website", validators=[
+    website = StringField("Website", validators=[
         Optional(), URL()])
         Optional(), URL()])
 
 
-    avatar = TextField("Avatar", validators=[
+    avatar = StringField("Avatar", validators=[
         Optional(), URL()])
         Optional(), URL()])
 
 
     signature = TextAreaField("Forum Signature", validators=[
     signature = TextAreaField("Forum Signature", validators=[
@@ -99,12 +99,12 @@ class ChangeUserDetailsForm(Form):
 
 
 
 
 class NewMessageForm(Form):
 class NewMessageForm(Form):
-    to_user = TextField("To User", validators=[
-        Required(message="A username is required.")])
-    subject = TextField("Subject", validators=[
-        Required(message="A subject is required.")])
+    to_user = StringField("To User", validators=[
+        DataRequired(message="A username is required.")])
+    subject = StringField("Subject", validators=[
+        DataRequired(message="A subject is required.")])
     message = TextAreaField("Message", validators=[
     message = TextAreaField("Message", validators=[
-        Required(message="A message is required.")])
+        DataRequired(message="A message is required.")])
 
 
     def validate_to_user(self, field):
     def validate_to_user(self, field):
         user = User.query.filter_by(username=field.data).first()
         user = User.query.filter_by(username=field.data).first()

+ 20 - 2
flaskbb/user/models.py

@@ -8,7 +8,6 @@
     :copyright: (c) 2014 by the FlaskBB Team.
     :copyright: (c) 2014 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
-import sys
 from datetime import datetime
 from datetime import datetime
 
 
 from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
 from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
@@ -16,6 +15,7 @@ from itsdangerous import SignatureExpired
 from werkzeug.security import generate_password_hash, check_password_hash
 from werkzeug.security import generate_password_hash, check_password_hash
 from flask import current_app, url_for
 from flask import current_app, url_for
 from flask.ext.login import UserMixin, AnonymousUserMixin
 from flask.ext.login import UserMixin, AnonymousUserMixin
+from flaskbb._compat import max_integer
 from flaskbb.extensions import db, cache
 from flaskbb.extensions import db, cache
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.forum.models import (Post, Topic, topictracker, TopicsRead,
 from flaskbb.forum.models import (Post, Topic, topictracker, TopicsRead,
@@ -316,7 +316,7 @@ class User(db.Model, UserMixin):
         return self.secondary_groups.filter(
         return self.secondary_groups.filter(
             groups_users.c.group_id == group.id).count() > 0
             groups_users.c.group_id == group.id).count() > 0
 
 
-    @cache.memoize(timeout=sys.maxint)
+    @cache.memoize(timeout=max_integer)
     def get_permissions(self, exclude=None):
     def get_permissions(self, exclude=None):
         """Returns a dictionary with all the permissions the user has.
         """Returns a dictionary with all the permissions the user has.
 
 
@@ -419,6 +419,17 @@ class User(db.Model, UserMixin):
         ForumsRead.query.filter_by(user_id=self.id).delete()
         ForumsRead.query.filter_by(user_id=self.id).delete()
         TopicsRead.query.filter_by(user_id=self.id).delete()
         TopicsRead.query.filter_by(user_id=self.id).delete()
 
 
+        # This should actually be handeld by the dbms.. but dunno why it doesnt
+        # work here
+        from flaskbb.forum.models import Forum
+
+        last_post_forums = Forum.query.\
+            filter_by(last_post_user_id=self.id).all()
+
+        for forum in last_post_forums:
+            forum.last_post_user_id = None
+            forum.save()
+
         db.session.delete(self)
         db.session.delete(self)
         db.session.commit()
         db.session.commit()
 
 
@@ -430,6 +441,7 @@ class Guest(AnonymousUserMixin):
     def permissions(self):
     def permissions(self):
         return self.get_permissions()
         return self.get_permissions()
 
 
+    @cache.memoize(timeout=max_integer)
     def get_permissions(self, exclude=None):
     def get_permissions(self, exclude=None):
         """Returns a dictionary with all permissions the user has"""
         """Returns a dictionary with all permissions the user has"""
         exclude = exclude or []
         exclude = exclude or []
@@ -444,6 +456,12 @@ class Guest(AnonymousUserMixin):
             perms[c.name] = getattr(group, c.name)
             perms[c.name] = getattr(group, c.name)
         return perms
         return perms
 
 
+    @classmethod
+    def invalidate_cache(cls):
+        """Invalidates this objects cached metadata."""
+
+        cache.delete_memoized(cls.get_permissions, cls)
+
 
 
 class PrivateMessage(db.Model):
 class PrivateMessage(db.Model):
     __tablename__ = "privatemessages"
     __tablename__ = "privatemessages"

+ 1 - 1
flaskbb/user/views.py

@@ -165,7 +165,7 @@ def new_message():
     to_user = request.args.get("to_user")
     to_user = request.args.get("to_user")
 
 
     if request.method == "POST":
     if request.method == "POST":
-        if "save_message" in request.form:
+        if "save_message" in request.form and form.validate():
             to_user = User.query.filter_by(username=form.to_user.data).first()
             to_user = User.query.filter_by(username=form.to_user.data).first()
 
 
             form.save(from_user=current_user.id,
             form.save(from_user=current_user.id,

+ 53 - 21
flaskbb/utils/helpers.py

@@ -12,16 +12,18 @@ import re
 import time
 import time
 import itertools
 import itertools
 import operator
 import operator
-from unicodedata import normalize
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 
 
-from flask import session
+from flask import session, url_for
 from flask.ext.themes2 import render_theme_template
 from flask.ext.themes2 import render_theme_template
 from flask.ext.login import current_user
 from flask.ext.login import current_user
 
 
 from postmarkup import render_bbcode
 from postmarkup import render_bbcode
+from markdown2 import markdown as render_markdown
+import unidecode
+from flaskbb._compat import range_method, text_type
 
 
-from flaskbb.extensions import redis
+from flaskbb.extensions import redis_store
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.settings import flaskbb_config
 
 
 _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
 _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
@@ -34,12 +36,12 @@ def slugify(text, delim=u'-'):
    :param text: The text which should be slugified
    :param text: The text which should be slugified
    :param delim: Default "-". The delimeter for whitespace
    :param delim: Default "-". The delimeter for whitespace
     """
     """
+    text = unidecode.unidecode(text)
     result = []
     result = []
     for word in _punct_re.split(text.lower()):
     for word in _punct_re.split(text.lower()):
-        word = normalize('NFKD', word).encode('ascii', 'ignore')
         if word:
         if word:
             result.append(word)
             result.append(word)
-    return unicode(delim.join(result))
+    return text_type(delim.join(result))
 
 
 
 
 def render_template(template, **context):
 def render_template(template, **context):
@@ -152,12 +154,19 @@ def forum_is_unread(forum, forumsread, user):
     # If the user hasn't visited a topic in the forum - therefore,
     # If the user hasn't visited a topic in the forum - therefore,
     # forumsread is None and we need to check if it is still unread
     # forumsread is None and we need to check if it is still unread
     if forum and not forumsread:
     if forum and not forumsread:
-        return forum.last_post.date_created > read_cutoff
+        return forum.last_post_created > read_cutoff
 
 
     try:
     try:
-        return forumsread.cleared > forum.last_post.date_created
+        # check if the forum has been cleared and if there is a new post
+        # since it have been cleared
+        if forum.last_post_created > forumsread.cleared:
+            if forum.last_post_created < forumsread.last_read:
+                return False
     except TypeError:
     except TypeError:
-        return forumsread.last_read < forum.last_post.date_created
+        pass
+
+    # else just check if the user has read the last post
+    return forum.last_post_created > forumsread.last_read
 
 
 
 
 def topic_is_unread(topic, topicsread, user, forumsread=None):
 def topic_is_unread(topic, topicsread, user, forumsread=None):
@@ -222,7 +231,7 @@ def mark_online(user_id, guest=False):
     else:
     else:
         all_users_key = 'online-users/%d' % (now // 60)
         all_users_key = 'online-users/%d' % (now // 60)
         user_key = 'user-activity/%s' % user_id
         user_key = 'user-activity/%s' % user_id
-    p = redis.pipeline()
+    p = redis_store.pipeline()
     p.sadd(all_users_key, user_id)
     p.sadd(all_users_key, user_id)
     p.set(user_key, now)
     p.set(user_key, now)
     p.expireat(all_users_key, expires)
     p.expireat(all_users_key, expires)
@@ -238,9 +247,9 @@ def get_last_user_activity(user_id, guest=False):
     :param guest: If the user is a guest (not signed in)
     :param guest: If the user is a guest (not signed in)
     """
     """
     if guest:
     if guest:
-        last_active = redis.get('guest-activity/%s' % user_id)
+        last_active = redis_store.get('guest-activity/%s' % user_id)
     else:
     else:
-        last_active = redis.get('user-activity/%s' % user_id)
+        last_active = redis_store.get('user-activity/%s' % user_id)
 
 
     if last_active is None:
     if last_active is None:
         return None
         return None
@@ -253,12 +262,12 @@ def get_online_users(guest=False):
     :param guest: If True, it will return the online guests
     :param guest: If True, it will return the online guests
     """
     """
     current = int(time.time()) // 60
     current = int(time.time()) // 60
-    minutes = xrange(flaskbb_config['ONLINE_LAST_MINUTES'])
+    minutes = range_method(flaskbb_config['ONLINE_LAST_MINUTES'])
     if guest:
     if guest:
-        return redis.sunion(['online-guests/%d' % (current - x)
-                             for x in minutes])
-    return redis.sunion(['online-users/%d' % (current - x)
-                         for x in minutes])
+        return redis_store.sunion(['online-guests/%d' % (current - x)
+                                   for x in minutes])
+    return redis_store.sunion(['online-users/%d' % (current - x)
+                               for x in minutes])
 
 
 
 
 def crop_title(title):
 def crop_title(title):
@@ -277,7 +286,11 @@ def render_markup(text):
 
 
     :param text: The text that should be rendered as bbcode
     :param text: The text that should be rendered as bbcode
     """
     """
-    return render_bbcode(text)
+    if flaskbb_config['MARKUP_TYPE'] == 'bbcode':
+        return render_bbcode(text)
+    elif flaskbb_config['MARKUP_TYPE'] == 'markdown':
+        return render_markdown(text, extras=['tables'])
+    return text
 
 
 
 
 def is_online(user):
 def is_online(user):
@@ -320,7 +333,6 @@ def time_delta_format(dt, default=None):
     note: when Babel1.0 is released, use format_timedelta/timedeltaformat
     note: when Babel1.0 is released, use format_timedelta/timedeltaformat
           instead
           instead
     """
     """
-
     if default is None:
     if default is None:
         default = 'just now'
         default = 'just now'
 
 
@@ -338,13 +350,33 @@ def time_delta_format(dt, default=None):
     )
     )
 
 
     for period, singular, plural in periods:
     for period, singular, plural in periods:
-
-        if not period:
+        if period < 1:
             continue
             continue
 
 
-        if period == 1:
+        if 1 <= period < 2:
             return u'%d %s ago' % (period, singular)
             return u'%d %s ago' % (period, singular)
         else:
         else:
             return u'%d %s ago' % (period, plural)
             return u'%d %s ago' % (period, plural)
 
 
     return default
     return default
+
+
+def format_quote(post):
+    """Returns a formatted quote depending on the markup language.
+
+    :param post: The quoted post.
+    """
+    if flaskbb_config['MARKUP_TYPE'] == 'markdown':
+        profile_url = url_for('user.profile', username=post.username)
+        content = "\n> ".join(post.content.strip().split('\n'))
+        quote = "**[{post.username}]({profile_url}) wrote:**\n> {content}\n".\
+                format(post=post, profile_url=profile_url, content=content)
+
+        return quote
+    else:
+        profile_url = url_for('user.profile', username=post.username,
+                              _external=True)
+        quote = '[b][url={profile_url}]{post.username}[/url] wrote:[/b][quote]{post.content}[/quote]\n'.\
+                format(post=post, profile_url=profile_url)
+
+        return quote

+ 26 - 15
flaskbb/utils/permissions.py

@@ -27,9 +27,11 @@ def check_perm(user, perm, forum, post_user_id=None):
     """
     """
     if can_moderate(user=user, forum=forum):
     if can_moderate(user=user, forum=forum):
         return True
         return True
+
     if post_user_id and user.is_authenticated():
     if post_user_id and user.is_authenticated():
         return user.permissions[perm] and user.id == post_user_id
         return user.permissions[perm] and user.id == post_user_id
-    return user.permissions[perm]
+
+    return not user.permissions['banned'] and user.permissions[perm]
 
 
 
 
 def is_moderator(user):
 def is_moderator(user):
@@ -92,36 +94,45 @@ def can_moderate(user, forum=None, perm=None):
     return user.permissions['super_mod'] or user.permissions['admin']
     return user.permissions['super_mod'] or user.permissions['admin']
 
 
 
 
-def can_edit_post(user, post_user_id, forum):
+def can_edit_post(user, post):
     """Check if the post can be edited by the user"""
     """Check if the post can be edited by the user"""
+    topic = post.topic
+
+    if can_moderate(user, topic.forum):
+        return True
 
 
-    return check_perm(user=user, perm='editpost', forum=forum,
-                      post_user_id=post_user_id)
+    if topic.locked or topic.forum.locked:
+        return False
 
 
+    return check_perm(user=user, perm='editpost', forum=post.topic.forum,
+                      post_user_id=post.user_id)
 
 
-def can_delete_post(user, post_user_id, forum):
-    """Check if the post can be deleted by the user"""
 
 
-    return check_perm(user=user, perm='deletepost', forum=forum,
-                      post_user_id=post_user_id)
+def can_delete_post(user, post):
+    """Check if the post can be deleted by the user"""
+    return check_perm(user=user, perm='deletepost', forum=post.topic.forum,
+                      post_user_id=post.user_id)
 
 
 
 
-def can_delete_topic(user, post_user_id, forum):
+def can_delete_topic(user, topic):
     """Check if the topic can be deleted by the user"""
     """Check if the topic can be deleted by the user"""
-
-    return check_perm(user=user, perm='deletetopic', forum=forum,
-                      post_user_id=post_user_id)
+    return check_perm(user=user, perm='deletetopic', forum=topic.forum,
+                      post_user_id=topic.user_id)
 
 
 
 
-def can_post_reply(user, forum):
+def can_post_reply(user, topic):
     """Check if the user is allowed to post in the forum"""
     """Check if the user is allowed to post in the forum"""
+    if can_moderate(user, topic.forum):
+        return True
 
 
-    return check_perm(user=user, perm='postreply', forum=forum)
+    if topic.locked or topic.forum.locked:
+        return False
+
+    return check_perm(user=user, perm='postreply', forum=topic.forum)
 
 
 
 
 def can_post_topic(user, forum):
 def can_post_topic(user, forum):
     """Checks if the user is allowed to create a new topic in the forum"""
     """Checks if the user is allowed to create a new topic in the forum"""
-
     return check_perm(user=user, perm='posttopic', forum=forum)
     return check_perm(user=user, perm='posttopic', forum=forum)
 
 
 
 

+ 87 - 6
flaskbb/utils/populate.py

@@ -8,14 +8,15 @@
     :copyright: (c) 2014 by the FlaskBB Team.
     :copyright: (c) 2014 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
     :license: BSD, see LICENSE for more details.
 """
 """
-from datetime import datetime
-
 from flaskbb.management.models import Setting, SettingsGroup
 from flaskbb.management.models import Setting, SettingsGroup
 from flaskbb.user.models import User, Group
 from flaskbb.user.models import User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category
 from flaskbb.forum.models import Post, Topic, Forum, Category
 
 
 
 
 def delete_settings_from_fixture(fixture):
 def delete_settings_from_fixture(fixture):
+    """
+    Deletes the settings from a fixture from the database.
+    """
     for settingsgroup in fixture:
     for settingsgroup in fixture:
         group = SettingsGroup.query.filter_by(key=settingsgroup[0]).first()
         group = SettingsGroup.query.filter_by(key=settingsgroup[0]).first()
 
 
@@ -26,6 +27,9 @@ def delete_settings_from_fixture(fixture):
 
 
 
 
 def create_settings_from_fixture(fixture):
 def create_settings_from_fixture(fixture):
+    """
+    Inserts the settings from a fixture into the database.
+    """
     for settingsgroup in fixture:
     for settingsgroup in fixture:
         group = SettingsGroup(
         group = SettingsGroup(
             key=settingsgroup[0],
             key=settingsgroup[0],
@@ -49,7 +53,51 @@ def create_settings_from_fixture(fixture):
             setting.save()
             setting.save()
 
 
 
 
+def update_settings_from_fixture(fixture, overwrite_group=False,
+                                 overwrite_setting=False):
+    """
+    Updates the database settings from a fixture.
+    Returns the number of updated groups and settings.
+    """
+    groups_count = 0
+    settings_count = 0
+    for settingsgroup in fixture:
+
+        group = SettingsGroup.query.filter_by(key=settingsgroup[0]).first()
+
+        if group is not None and overwrite_group or group is None:
+            groups_count += 1
+            group = SettingsGroup(
+                key=settingsgroup[0],
+                name=settingsgroup[1]['name'],
+                description=settingsgroup[1]['description']
+            )
+
+            group.save()
+
+        for settings in settingsgroup[1]['settings']:
+
+            setting = Setting.query.filter_by(key=settings[0]).first()
+
+            if setting is not None and overwrite_setting or setting is None:
+                settings_count += 1
+                setting = Setting(
+                    key=settings[0],
+                    value=settings[1]['value'],
+                    value_type=settings[1]['value_type'],
+                    name=settings[1]['name'],
+                    description=settings[1]['description'],
+                    extra=settings[1].get('extra', ""),
+                    settingsgroup=group.key
+                )
+                setting.save()
+    return groups_count, settings_count
+
+
 def create_default_settings():
 def create_default_settings():
+    """
+    Creates the default settings
+    """
     from flaskbb.fixtures.settings import fixture
     from flaskbb.fixtures.settings import fixture
     create_settings_from_fixture(fixture)
     create_settings_from_fixture(fixture)
 
 
@@ -76,8 +124,13 @@ def create_admin_user(username, password, email):
     Creates the administrator user
     Creates the administrator user
     """
     """
     admin_group = Group.query.filter_by(admin=True).first()
     admin_group = Group.query.filter_by(admin=True).first()
-    user = User(username=username, password=password, email=email,
-                date_joined=datetime.utcnow(), primary_group_id=admin_group.id)
+    user = User()
+
+    user.username = username
+    user.password = password
+    user.email = email
+    user.primary_group_id = admin_group.id
+
     user.save()
     user.save()
 
 
 
 
@@ -86,7 +139,6 @@ def create_welcome_forum():
     This will create the `welcome forum` that nearly every
     This will create the `welcome forum` that nearly every
     forum software has after the installation process is finished
     forum software has after the installation process is finished
     """
     """
-
     if User.query.count() < 1:
     if User.query.count() < 1:
         raise "You need to create the admin user first!"
         raise "You need to create the admin user first!"
 
 
@@ -106,7 +158,10 @@ def create_welcome_forum():
 
 
 
 
 def create_test_data():
 def create_test_data():
-
+    """
+    Creates 5 users, 2 categories and 2 forums in each category. It also opens
+    a new topic topic in each forum with a post.
+    """
     create_default_groups()
     create_default_groups()
     create_default_settings()
     create_default_settings()
 
 
@@ -150,3 +205,29 @@ def create_test_data():
             post = Post()
             post = Post()
             post.content = "Test Post"
             post.content = "Test Post"
             post.save(user=user2, topic=topic)
             post.save(user=user2, topic=topic)
+
+
+def insert_mass_data():
+    """
+    Creates 100 topics in the first forum and each topic has 100 posts.
+    """
+    user1 = User.query.filter_by(id=1).first()
+    user2 = User.query.filter_by(id=2).first()
+    forum = Forum.query.filter_by(id=1).first()
+
+    # create 1000 topics
+    for i in range(1, 101):
+
+        # create a topic
+        topic = Topic()
+        post = Post()
+
+        topic.title = "Test Title %s" % i
+        post.content = "Test Content"
+        topic.save(post=post, user=user1, forum=forum)
+
+        # create 100 posts in each topic
+        for j in range(1, 100):
+            post = Post()
+            post.content = "Test Post"
+            post.save(user=user2, topic=topic)

+ 40 - 1
manage.py

@@ -13,6 +13,7 @@
 import sys
 import sys
 
 
 from flask import current_app
 from flask import current_app
+from werkzeug.utils import import_string
 from sqlalchemy.exc import IntegrityError, OperationalError
 from sqlalchemy.exc import IntegrityError, OperationalError
 from flask.ext.script import (Manager, Shell, Server, prompt, prompt_pass,
 from flask.ext.script import (Manager, Shell, Server, prompt, prompt_pass,
                               prompt_bool)
                               prompt_bool)
@@ -22,7 +23,8 @@ from flaskbb import create_app
 from flaskbb.extensions import db
 from flaskbb.extensions import db
 from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
 from flaskbb.utils.populate import (create_test_data, create_welcome_forum,
                                     create_admin_user, create_default_groups,
                                     create_admin_user, create_default_groups,
-                                    create_default_settings)
+                                    create_default_settings, insert_mass_data,
+                                    update_settings_from_fixture)
 
 
 # Use the development configuration if available
 # Use the development configuration if available
 try:
 try:
@@ -60,6 +62,35 @@ def dropdb():
     db.drop_all()
     db.drop_all()
 
 
 
 
+@manager.option('-s', '--settings', dest="settings")
+@manager.option('-f', '--force', dest="force")
+def update(settings=None, force=False):
+    """Updates the settings via a fixture. All fixtures have to be placed
+    in the `fixture`.
+    Usage: python manage.py update -s your_fixture
+    """
+    try:
+        fixture = import_string(
+            "flaskbb.fixtures.{}".format(settings)
+        )
+        fixture = fixture.fixture
+    except ImportError:
+        raise "{} fixture is not available".format(settings)
+
+    if force:
+        count = update_settings_from_fixture(fixture, overwrite_group=True,
+                                             overwrite_setting=True)
+        app.logger.info(
+            "{} groups and {} settings forcefully updated."
+            .format(count[0], count[1])
+        )
+    else:
+        count = update_settings_from_fixture(fixture)
+        app.logger.info(
+            "{} groups and {} settings updated.".format(count[0], count[1])
+        )
+
+
 @manager.command
 @manager.command
 def createall(dropdb=False, createdb=False):
 def createall(dropdb=False, createdb=False):
     """Creates the database with some testing content.
     """Creates the database with some testing content.
@@ -136,5 +167,13 @@ def initflaskbb(username=None, password=None, email=None):
     app.logger.info("Congratulations! FlaskBB has been successfully installed")
     app.logger.info("Congratulations! FlaskBB has been successfully installed")
 
 
 
 
+@manager.command
+def insertmassdata():
+    """Warning: This can take a long time!.
+    Creates 100 topics and each topic contains 100 posts.
+    """
+    insert_mass_data()
+
+
 if __name__ == "__main__":
 if __name__ == "__main__":
     manager.run()
     manager.run()

+ 18 - 15
requirements.txt

@@ -1,32 +1,35 @@
 Flask==0.10.1
 Flask==0.10.1
-Flask-And-Redis==0.5
 Flask-Cache==0.13.1
 Flask-Cache==0.13.1
 Flask-DebugToolbar==0.9.0
 Flask-DebugToolbar==0.9.0
 Flask-Login==0.2.11
 Flask-Login==0.2.11
-Flask-Mail==0.9.0
+Flask-Mail==0.9.1
 Flask-Migrate==1.2.0
 Flask-Migrate==1.2.0
-Flask-Plugins==1.4
-Flask-SQLAlchemy==1.0
+Flask-Plugins==1.5
+Flask-SQLAlchemy==2.0
 Flask-Script==2.0.5
 Flask-Script==2.0.5
 Flask-Themes2==0.1.3
 Flask-Themes2==0.1.3
-Flask-WTF==0.10
-Flask-WhooshAlchemy==0.56
+Flask-WTF==0.10.2
 Jinja2==2.7.3
 Jinja2==2.7.3
 Mako==1.0.0
 Mako==1.0.0
 MarkupSafe==0.23
 MarkupSafe==0.23
 Pygments==1.6
 Pygments==1.6
-SQLAlchemy==0.9.7
+SQLAlchemy==0.9.8
 WTForms==2.0.1
 WTForms==2.0.1
 Werkzeug==0.9.6
 Werkzeug==0.9.6
 Whoosh==2.6.0
 Whoosh==2.6.0
-alembic==0.6.5
+alembic==0.6.7
 blinker==1.3
 blinker==1.3
+cov-core==1.14.0
+coverage==3.7.1
 itsdangerous==0.24
 itsdangerous==0.24
-py==1.4.22
-pytest==2.6.0
+py==1.4.25
+pytest==2.6.3
+pytest-cov==1.8.0
 pytest-random==0.02
 pytest-random==0.02
-pytest-cov==1.7.0
-redis==2.10.1
-simplejson==3.6.0
-
-https://github.com/frol/postmarkup/tarball/master#egg=postmarkup
+redis==2.10.3
+simplejson==3.6.4
+flask-redis==0.0.6
+unidecode==0.04.16
+markdown2==2.3.0
+https://github.com/frol/postmarkup/tarball/master#egg=postmarkup
+https://github.com/jshipley/Flask-WhooshAlchemy/archive/master.zip#egg=Flask-Whooshalchemy

+ 5 - 3
setup.py

@@ -40,7 +40,7 @@ setup(
     platforms='any',
     platforms='any',
     install_requires=[
     install_requires=[
         'Flask',
         'Flask',
-        'Flask-And-Redis',
+        'Flask-Redis',
         'Flask-Cache',
         'Flask-Cache',
         'Flask-DebugToolbar',
         'Flask-DebugToolbar',
         'Flask-Login',
         'Flask-Login',
@@ -69,10 +69,12 @@ setup(
         'pytest-cov',
         'pytest-cov',
         'redis',
         'redis',
         'simplejson',
         'simplejson',
-        'postmarkup'
+        'postmarkup',
+        'unidecode'
     ],
     ],
     dependency_links=[
     dependency_links=[
-        'https://github.com/frol/postmarkup/tarball/master#egg=postmarkup'
+        'https://github.com/frol/postmarkup/tarball/master#egg=postmarkup',
+        'https://github.com/jshipley/Flask-WhooshAlchemy/archive/master.zip#egg=Flask-WhooshAlchemy'
     ],
     ],
     classifiers=[
     classifiers=[
         'Development Status :: 4 - Beta',
         'Development Status :: 4 - Beta',

+ 1 - 1
tests/unit/test_forum_models.py

@@ -380,7 +380,7 @@ def test_topic_move(topic):
 
 
     assert topic.move(forum_other)
     assert topic.move(forum_other)
 
 
-    assert forum_old.topics == []
+    assert forum_old.topics.all() == []
     assert forum_old.last_post_id is None
     assert forum_old.last_post_id is None
 
 
     assert forum_old.topic_count == 0
     assert forum_old.topic_count == 0

+ 23 - 23
tests/unit/utils/test_permissions.py

@@ -13,13 +13,13 @@ def test_moderator_permissions_in_forum(
 
 
     assert moderator_user in forum.moderators
     assert moderator_user in forum.moderators
 
 
-    assert can_post_reply(moderator_user, forum)
+    assert can_post_reply(moderator_user, topic)
     assert can_post_topic(moderator_user, forum)
     assert can_post_topic(moderator_user, forum)
-    assert can_edit_post(moderator_user, topic.user_id, forum)
+    assert can_edit_post(moderator_user, topic.first_post)
 
 
     assert can_moderate(moderator_user, forum)
     assert can_moderate(moderator_user, forum)
-    assert can_delete_post(moderator_user, topic.user_id, forum)
-    assert can_delete_topic(moderator_user, topic.user_id, forum)
+    assert can_delete_post(moderator_user, topic.first_post)
+    assert can_delete_topic(moderator_user, topic)
 
 
 
 
 def test_moderator_permissions_without_forum(
 def test_moderator_permissions_without_forum(
@@ -32,17 +32,17 @@ def test_moderator_permissions_without_forum(
     assert not moderator_user in forum.moderators
     assert not moderator_user in forum.moderators
     assert not can_moderate(moderator_user, forum)
     assert not can_moderate(moderator_user, forum)
 
 
-    assert can_post_reply(moderator_user, forum)
+    assert can_post_reply(moderator_user, topic)
     assert can_post_topic(moderator_user, forum)
     assert can_post_topic(moderator_user, forum)
 
 
-    assert not can_edit_post(moderator_user, topic.user_id, forum)
-    assert not can_delete_post(moderator_user, topic.user_id, forum)
-    assert not can_delete_topic(moderator_user, topic.user_id, forum)
+    assert not can_edit_post(moderator_user, topic.first_post)
+    assert not can_delete_post(moderator_user, topic.first_post)
+    assert not can_delete_topic(moderator_user, topic)
 
 
     # Test with own topic
     # Test with own topic
-    assert can_delete_post(moderator_user, topic_moderator.user_id, forum)
-    assert can_delete_topic(moderator_user, topic_moderator.user_id, forum)
-    assert can_edit_post(moderator_user, topic_moderator.user_id, forum)
+    assert can_delete_post(moderator_user, topic_moderator.first_post)
+    assert can_delete_topic(moderator_user, topic_moderator)
+    assert can_edit_post(moderator_user, topic_moderator.first_post)
 
 
     # Test moderator permissions
     # Test moderator permissions
     assert can_edit_user(moderator_user)
     assert can_edit_user(moderator_user)
@@ -53,12 +53,12 @@ def test_normal_permissions(forum, user, topic):
     """Test the permissions for a normal user."""
     """Test the permissions for a normal user."""
     assert not can_moderate(user, forum)
     assert not can_moderate(user, forum)
 
 
-    assert can_post_reply(user, forum)
+    assert can_post_reply(user, topic)
     assert can_post_topic(user, forum)
     assert can_post_topic(user, forum)
 
 
-    assert can_edit_post(user, topic.user_id, forum)
-    assert not can_delete_post(user, topic.user_id, forum)
-    assert not can_delete_topic(user, topic.user_id, forum)
+    assert can_edit_post(user, topic.first_post)
+    assert not can_delete_post(user, topic.first_post)
+    assert not can_delete_topic(user, topic)
 
 
     assert not can_edit_user(user)
     assert not can_edit_user(user)
     assert not can_ban_user(user)
     assert not can_ban_user(user)
@@ -68,12 +68,12 @@ def test_admin_permissions(forum, admin_user, topic):
     """Test the permissions for a admin user."""
     """Test the permissions for a admin user."""
     assert can_moderate(admin_user, forum)
     assert can_moderate(admin_user, forum)
 
 
-    assert can_post_reply(admin_user, forum)
+    assert can_post_reply(admin_user, topic)
     assert can_post_topic(admin_user, forum)
     assert can_post_topic(admin_user, forum)
 
 
-    assert can_edit_post(admin_user, topic.user_id, forum)
-    assert can_delete_post(admin_user, topic.user_id, forum)
-    assert can_delete_topic(admin_user, topic.user_id, forum)
+    assert can_edit_post(admin_user, topic.first_post)
+    assert can_delete_post(admin_user, topic.first_post)
+    assert can_delete_topic(admin_user, topic)
 
 
     assert can_edit_user(admin_user)
     assert can_edit_user(admin_user)
     assert can_ban_user(admin_user)
     assert can_ban_user(admin_user)
@@ -83,12 +83,12 @@ def test_super_moderator_permissions(forum, super_moderator_user, topic):
     """Test the permissions for a super moderator user."""
     """Test the permissions for a super moderator user."""
     assert can_moderate(super_moderator_user, forum)
     assert can_moderate(super_moderator_user, forum)
 
 
-    assert can_post_reply(super_moderator_user, forum)
+    assert can_post_reply(super_moderator_user, topic)
     assert can_post_topic(super_moderator_user, forum)
     assert can_post_topic(super_moderator_user, forum)
 
 
-    assert can_edit_post(super_moderator_user, topic.user_id, forum)
-    assert can_delete_post(super_moderator_user, topic.user_id, forum)
-    assert can_delete_topic(super_moderator_user, topic.user_id, forum)
+    assert can_edit_post(super_moderator_user, topic.first_post)
+    assert can_delete_post(super_moderator_user, topic.first_post)
+    assert can_delete_topic(super_moderator_user, topic)
 
 
     assert can_edit_user(super_moderator_user)
     assert can_edit_user(super_moderator_user)
     assert can_ban_user(super_moderator_user)
     assert can_ban_user(super_moderator_user)