Browse Source

Merge pull request #14 from sh4nks/develop

Added new features and fixed a few bugs
sh4nks 11 years ago
parent
commit
22fe64a27c

+ 23 - 8
README.md

@@ -18,10 +18,10 @@ using the micro framework Flask.
 ## TODO
 
 * Searching for members, posts,...
-* "Link to"-Forum type
-* Move a topic in a other forum
+* ~~"Link to"-Forum type~~
+* ~~Move a topic in a other forum~~
 * Merging 2 topics together
-* Reporting posts
+* ~~Reporting posts~~
 * Userstyles (e.q.: colored username)
 * ~~Database migrations~~
 * A own theme ~~and make FlaskBB themable with Flask-Themes2~~
@@ -46,6 +46,7 @@ using the micro framework Flask.
 
 
 ### OPTIONAL DEPENDENCIES
+
 * [Pygmens](http://pygments.org/) - For code highlighting
 * [Redis](http://redis.io/) - For counting the online guests
 
@@ -55,7 +56,6 @@ using the micro framework Flask.
 * Create a virtualenv
     * Install virtualenvwrapper with your package manager or via
         * `sudo pip install virtualenvwrapper`
-
     * Add these lines to your `.bashrc`
 
             export WORKON_HOME=$HOME/.virtualenvs  # Location for your virtualenvs
@@ -63,24 +63,39 @@ using the micro framework Flask.
 
     * Create a new virtualenv
         * `mkvirtualenv -a /path/to/flaskbb -p $(which python2) flaskbb`
-
     * and finally activate it
         * `workon flaskbb`
-
     * For more options visit the documentation [here](http://virtualenvwrapper.readthedocs.org/en/latest/index.html).
 
+
 * Install the dependencies
     * `pip install -r requirements.txt`
+    * **NOTE**: If you are using pip 1.5 you need to add these parameters: ``--allow-external postmarkup --allow-unverified postmarkup``
 * Configuration (_adjust them accordingly to your needs_)
     * For development copy `flaskbb/configs/development.py.example` to `flaskbb/configs/development.py`
     * For production copy `flaskbb/configs/production.py.example` to `flaskbb/configs/production.py`
-* Create the database with some example content
-    * `python manage.py createall`
+* Database creation
+    * **Development:** Create the database with some example content
+        * `python manage.py createall`
+    * **Production:** Create the database and the admin user
+        * `python manage.py initflaskbb`
 * Run the development server
     * `python manage.py runserver`
 * Visit [localhost:8080](http://localhost:8080)
 
 
+## Upgrading
+
+* Upgrading from a previous installation
+    * Pull the latest changes from the repository
+    * `git pull`
+* See if the example config has changed and adjust the settings to your needs
+    * `diff flaskbb/configs/production.py flaskbb/configs/production.py.example`
+    * `$EDITOR flaskbb/configs/production.py`
+* Upgrade the database to the latest revision
+    * `python manage.py db upgrade head`
+
+
 ## LICENSE
 
 [BSD LICENSE](http://flask.pocoo.org/docs/license/#flask-license)

+ 28 - 1
flaskbb/admin/forms.py

@@ -254,6 +254,10 @@ class ForumForm(Form):
         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'")
+
     moderators = TextField(
         "Moderators",
         description="Comma seperated usernames. Leave it blank if you do not \
@@ -270,6 +274,12 @@ class ForumForm(Form):
         description="Disable new posts and topics in this forum."
     )
 
+    def validate_external(self, field):
+        if hasattr(self, "forum"):
+            if self.forum.topics:
+                raise ValidationError("You cannot convert a forum that \
+                                       contain topics in a external link")
+
     def validate_show_moderators(self, field):
         if field.data and not self.moderators.data:
             raise ValidationError("You also need to specify some moderators.")
@@ -299,10 +309,16 @@ class ForumForm(Form):
                     raise ValidationError("User %s not found" % moderator)
             field.data = approved_moderators
 
+        else:
+            field.data = approved_moderators
+
     def save(self):
         forum = Forum(title=self.title.data,
                       description=self.description.data,
-                      position=self.position.data)
+                      position=self.position.data,
+                      external=self.external.data,
+                      show_moderators=self.show_moderators.data,
+                      locked=self.locked.data)
 
         if self.moderators.data:
             # is already validated
@@ -313,6 +329,17 @@ class ForumForm(Form):
         return forum.save()
 
 
+class EditForumForm(ForumForm):
+    def __init__(self, forum, *args, **kwargs):
+        self.forum = forum
+        kwargs['obj'] = self.forum
+        super(ForumForm, self).__init__(*args, **kwargs)
+
+
+class AddForumForm(ForumForm):
+    pass
+
+
 class CategoryForm(Form):
     title = TextField("Category title", validators=[
         Required(message="Category title required")])

+ 69 - 21
flaskbb/admin/views.py

@@ -9,18 +9,21 @@
     :license: BSD, see LICENSE for more details.
 """
 import sys
+from datetime import datetime
 
 from flask import (Blueprint, current_app, request, redirect, url_for, flash,
                    __version__ as flask_version)
+from flask.ext.login import current_user
 
 from flaskbb import __version__ as flaskbb_version
 from flaskbb.utils.helpers import render_template
 from flaskbb.utils.decorators import admin_required
 from flaskbb.extensions import db
 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, Report
 from flaskbb.admin.forms import (AddUserForm, EditUserForm, AddGroupForm,
-                                 EditGroupForm, ForumForm, CategoryForm)
+                                 EditGroupForm, EditForumForm, AddForumForm,
+                                 CategoryForm)
 
 
 admin = Blueprint("admin", __name__)
@@ -71,6 +74,62 @@ def forums():
     return render_template("admin/forums.html", categories=categories)
 
 
+@admin.route("/reports")
+@admin_required
+def reports():
+    page = request.args.get("page", 1, type=int)
+    reports = Report.query.\
+        order_by(Report.id.asc()).\
+        paginate(page, current_app.config['USERS_PER_PAGE'], False)
+
+    return render_template("admin/reports.html", reports=reports)
+
+
+@admin.route("/reports/unread")
+@admin_required
+def unread_reports():
+    page = request.args.get("page", 1, type=int)
+    reports = Report.query.\
+        filter(Report.zapped == None).\
+        order_by(Report.id.desc()).\
+        paginate(page, current_app.config['USERS_PER_PAGE'], False)
+
+    return render_template("admin/unread_reports.html", reports=reports)
+
+
+@admin.route("/reports/<int:report_id>/markread")
+@admin.route("/reports/markread")
+@admin_required
+def report_markread(report_id=None):
+    # mark single report as read
+    if report_id:
+
+        report = Report.query.filter_by(id=report_id).first_or_404()
+        if report.zapped:
+            flash("Report %s is already marked as read" % report.id, "success")
+            return redirect(url_for("admin.reports"))
+
+        report.zapped_by = current_user.id
+        report.zapped = datetime.utcnow()
+        report.save()
+        flash("Report %s marked as read" % report.id, "success")
+        return redirect(url_for("admin.reports"))
+
+    # mark all as read
+    reports = Report.query.filter(Report.zapped == None).all()
+    report_list = []
+    for report in reports:
+        report.zapped_by = current_user.id
+        report.zapped = datetime.utcnow()
+        report_list.append(report)
+
+    db.session.add_all(report_list)
+    db.session.commit()
+
+    flash("All reports were marked as read", "success")
+    return redirect(url_for("admin.reports"))
+
+
 @admin.route("/users/<int:user_id>/edit", methods=["GET", "POST"])
 @admin_required
 def edit_user(user_id):
@@ -84,15 +143,7 @@ def edit_user(user_id):
     form = EditUserForm(user)
     form.secondary_groups.query = secondary_group_query
     if form.validate_on_submit():
-        user.username = form.username.data
-        user.email = form.email.data
-        user.birthday = form.birthday.data
-        user.gender = form.gender.data
-        user.website = form.website.data
-        user.location = form.location.data
-        user.signature = form.signature.data
-        user.avatar = form.avatar.data
-        user.notes = form.notes.data
+        form.populate_obj(user)
         user.primary_group_id = form.primary_group.data.id
 
        # Don't override the password
@@ -199,16 +250,9 @@ def add_group():
 def edit_forum(forum_id):
     forum = Forum.query.filter_by(id=forum_id).first_or_404()
 
-    form = ForumForm()
-
+    form = EditForumForm(forum)
     if form.validate_on_submit():
-        forum.title = form.title.data
-        forum.description = form.description.data
-        forum.position = form.position.data
-        forum.locked = form.locked.data
-        forum.category_id = form.category.data.id
-        forum.show_moderators = form.show_moderators.data
-
+        form.populate_obj(forum)
         forum.save(moderators=form.moderators.data)
 
         flash("Forum successfully edited.", "success")
@@ -218,6 +262,7 @@ def edit_forum(forum_id):
         form.description.data = forum.description
         form.position.data = forum.position
         form.category.data = forum.category
+        form.external.data = forum.external
         form.locked.data = forum.locked
         form.show_moderators.data = forum.show_moderators
 
@@ -249,7 +294,7 @@ def delete_forum(forum_id):
 @admin.route("/forums/<int:category_id>/add", methods=["GET", "POST"])
 @admin_required
 def add_forum(category_id=None):
-    form = ForumForm()
+    form = AddForumForm()
 
     if form.validate_on_submit():
         form.save()
@@ -265,6 +310,7 @@ def add_forum(category_id=None):
 
 
 @admin.route("/category/add", methods=["GET", "POST"])
+@admin_required
 def add_category():
     form = CategoryForm()
 
@@ -278,6 +324,7 @@ def add_category():
 
 
 @admin.route("/category/<int:category_id>/edit", methods=["GET", "POST"])
+@admin_required
 def edit_category(category_id):
     category = Category.query.filter_by(id=category_id).first_or_404()
 
@@ -296,6 +343,7 @@ def edit_category(category_id):
 
 
 @admin.route("/category/<int:category_id>/delete", methods=["GET", "POST"])
+@admin_required
 def delete_category(category_id):
     category = Category.query.filter_by(id=category_id).first_or_404()
 

+ 1 - 1
flaskbb/app.py

@@ -223,7 +223,7 @@ def configure_logging(app):
     if app.config["SEND_LOGS"]:
         mail_handler = \
             SMTPHandler(app.config['MAIL_SERVER'],
-                        app.config['DEFAULT_MAIL_SENDER'],
+                        app.config['MAIL_DEFAULT_SENDER'],
                         app.config['ADMINS'],
                         'application error, no admins specified',
                         (

+ 1 - 0
flaskbb/configs/default.py

@@ -72,6 +72,7 @@ class DefaultConfig(object):
     MAIL_USERNAME = "noreply@example.org"
     MAIL_PASSWORD = ""
     MAIL_DEFAULT_SENDER = ("Default Sender", "noreply@example.org")
+    # Where to logger should send the emails to
     ADMINS = ["admin@example.org"]
 
     ## App specific configs

+ 1 - 0
flaskbb/configs/development.py.example

@@ -47,4 +47,5 @@ class DevelopmentConfig(DefaultConfig):
     MAIL_USERNAME = "flaskbb@gmail.com"
     MAIL_PASSWORD = "your_password"
     MAIL_DEFAULT_SENDER = "flaskbb@gmail.com"
+    # Where to logger should send the emails to
     ADMINS = ["flaskbb@gmail.com"]

+ 1 - 0
flaskbb/configs/production.py.example

@@ -56,6 +56,7 @@ class ProductionConfig(DefaultConfig):
     MAIL_USERNAME = "your_username@gmail.com"
     MAIL_PASSWORD = "your_password"
     MAIL_DEFAULT_SENDER = ("Your Name", "your_username@gmail.com")
+    # Where to logger should send the emails to
     ADMINS = ["your_admin_user@gmail.com"]
 
 

+ 12 - 1
flaskbb/forum/forms.py

@@ -12,7 +12,7 @@ from flask.ext.wtf import Form
 from wtforms import TextAreaField, TextField
 from wtforms.validators import Required
 
-from flaskbb.forum.models import Topic, Post
+from flaskbb.forum.models import Topic, Post, Report
 
 
 class QuickreplyForm(Form):
@@ -43,3 +43,14 @@ class NewTopicForm(ReplyForm):
         topic = Topic(title=self.title.data)
         post = Post(content=self.content.data)
         return topic.save(user=user, forum=forum, post=post)
+
+
+class ReportForm(Form):
+    reason = TextAreaField("Reason", validators=[
+        Required(message="Please insert a reason why you want to report this \
+                          post")
+    ])
+
+    def save(self, user, post):
+        report = Report(**self.data)
+        return report.save(user, post)

+ 140 - 29
flaskbb/forum/models.py

@@ -18,16 +18,21 @@ from flaskbb.utils.query import TopicQuery
 
 moderators = db.Table(
     'moderators',
-    db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
+    db.Column('user_id', db.Integer(), db.ForeignKey('users.id'),
+              nullable=False),
     db.Column('forum_id', db.Integer(),
-              db.ForeignKey('forums.id', use_alter=True, name="fk_forum_id")))
+              db.ForeignKey('forums.id', use_alter=True, name="fk_forum_id"),
+              nullable=False))
 
 
 topictracker = db.Table(
     'topictracker',
-    db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
+    db.Column('user_id', db.Integer(), db.ForeignKey('users.id'),
+              nullable=False),
     db.Column('topic_id', db.Integer(),
-              db.ForeignKey('topics.id', use_alter=True, name="fk_topic_id")))
+              db.ForeignKey('topics.id',
+                            use_alter=True, name="fk_tracker_topic_id"),
+              nullable=False))
 
 
 class TopicsRead(db.Model):
@@ -37,7 +42,7 @@ class TopicsRead(db.Model):
                         primary_key=True)
     topic_id = db.Column(db.Integer,
                          db.ForeignKey("topics.id", use_alter=True,
-                                       name="fk_topic_id"),
+                                       name="fk_tr_topic_id"),
                          primary_key=True)
     forum_id = db.Column(db.Integer,
                          db.ForeignKey("forums.id", use_alter=True,
@@ -63,7 +68,7 @@ class ForumsRead(db.Model):
                         primary_key=True)
     forum_id = db.Column(db.Integer,
                          db.ForeignKey("topics.id", use_alter=True,
-                                       name="fk_forum_id"),
+                                       name="fk_fr_forum_id"),
                          primary_key=True)
     last_read = db.Column(db.DateTime, default=datetime.utcnow())
     cleared = db.Column(db.DateTime)
@@ -79,19 +84,69 @@ class ForumsRead(db.Model):
         return self
 
 
+class Report(db.Model):
+    __tablename__ = "reports"
+
+    id = db.Column(db.Integer, primary_key=True)
+    reporter_id = db.Column(db.Integer, db.ForeignKey("users.id"),
+                            nullable=False)
+    reported = db.Column(db.DateTime, default=datetime.utcnow())
+    post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=False)
+    zapped = db.Column(db.DateTime)
+    zapped_by = db.Column(db.Integer, db.ForeignKey("users.id"))
+    reason = db.Column(db.String(63))
+
+    post = db.relationship("Post", backref="report", lazy="joined")
+    reporter = db.relationship("User", lazy="joined",
+                               foreign_keys=[reporter_id])
+    zapper = db.relationship("User", lazy="joined", foreign_keys=[zapped_by])
+
+    def save(self, post=None, user=None):
+        """Saves a report.
+
+        :param post: The post that should be reported
+
+        :param user: The user who has reported the post
+
+        :param reason: The reason why the user has reported the post
+        """
+
+        if self.id:
+            db.session.add(self)
+            db.session.commit()
+            return self
+
+        if post and user:
+            self.reporter_id = user.id
+            self.reported = datetime.utcnow()
+            self.post_id = post.id
+
+        db.session.add(self)
+        db.session.commit()
+        return self
+
+    def delete(self):
+        db.session.delete(self)
+        db.session.commit()
+        return self
+
+
 class Post(db.Model):
     __tablename__ = "posts"
 
     id = db.Column(db.Integer, primary_key=True)
-    topic_id = db.Column(db.Integer, db.ForeignKey("topics.id", use_alter=True,
-                                                   name="fk_topic_id",
-                                                   ondelete="CASCADE"))
-    user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
-    username = db.Column(db.String, nullable=False)
+    topic_id = db.Column(db.Integer,
+                         db.ForeignKey("topics.id",
+                                       use_alter=True,
+                                       name="fk_post_topic_id",
+                                       ondelete="CASCADE"),
+                         nullable=False)
+    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
+    username = db.Column(db.String(15), nullable=False)
     content = db.Column(db.Text, nullable=False)
     date_created = db.Column(db.DateTime, default=datetime.utcnow())
     date_modified = db.Column(db.DateTime)
-    modified_by = db.Column(db.String)
+    modified_by = db.Column(db.String(15))
 
     # Methods
     def __repr__(self):
@@ -176,11 +231,14 @@ class Topic(db.Model):
     query_class = TopicQuery
 
     id = db.Column(db.Integer, primary_key=True)
-    forum_id = db.Column(db.Integer, db.ForeignKey("forums.id", use_alter=True,
-                                                   name="fk_forum_id"))
-    title = db.Column(db.String)
+    forum_id = db.Column(db.Integer,
+                         db.ForeignKey("forums.id",
+                                       use_alter=True,
+                                       name="fk_topic_forum_id"),
+                         nullable=False)
+    title = db.Column(db.String(63), nullable=False)
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
-    username = db.Column(db.String, nullable=False)
+    username = db.Column(db.String(15), nullable=False)
     date_created = db.Column(db.DateTime, default=datetime.utcnow())
     last_updated = db.Column(db.DateTime, default=datetime.utcnow())
     locked = db.Column(db.Boolean, default=False)
@@ -223,6 +281,34 @@ class Topic(db.Model):
         """
         return "<{} {}>".format(self.__class__.__name__, self.id)
 
+    def move(self, forum):
+        """Moves a topic to the given forum.
+        Returns True if it could successfully move the topic to forum.
+
+        :param forum: The new forum for the topic
+        """
+
+        # if the target forum is the current forum, abort
+        if self.forum_id == forum.id:
+            return False
+
+        old_forum = self.forum
+        self.forum.post_count -= self.post_count
+        self.forum.topic_count -= 1
+        self.forum_id = forum.id
+
+        forum.post_count += self.post_count
+        forum.topic_count += 1
+
+        db.session.commit()
+
+        forum.update_last_post()
+        old_forum.update_last_post()
+
+        TopicsRead.query.filter_by(topic_id=self.id).delete()
+
+        return True
+
     def save(self, user=None, forum=None, post=None):
         """Saves a topic and returns the topic object. If no parameters are
         given, it will only update the topic.
@@ -371,7 +457,7 @@ class Topic(db.Model):
                               TopicsRead.last_read < Topic.last_updated)).\
                 count()
 
-            #No unread topics available - trying to mark the forum as read
+            # No unread topics available - trying to mark the forum as read
             if unread_count == 0:
                 forumread = ForumsRead.query.\
                     filter(ForumsRead.user_id == user.id,
@@ -400,15 +486,17 @@ class Forum(db.Model):
     __tablename__ = "forums"
 
     id = db.Column(db.Integer, primary_key=True)
-    category_id = db.Column(db.Integer, db.ForeignKey("categories.id"))
-    title = db.Column(db.String)
-    description = db.Column(db.String)
-    position = db.Column(db.Integer, default=1)
-    locked = db.Column(db.Boolean, default=False)
-    show_moderators = db.Column(db.Boolean, default=False)
-
-    post_count = db.Column(db.Integer, default=0)
-    topic_count = db.Column(db.Integer, default=0)
+    category_id = db.Column(db.Integer, db.ForeignKey("categories.id"),
+                            nullable=False)
+    title = db.Column(db.String(15), nullable=False)
+    description = db.Column(db.String(255))
+    position = db.Column(db.Integer, default=1, nullable=False)
+    locked = db.Column(db.Boolean, default=False, nullable=False)
+    show_moderators = db.Column(db.Boolean, default=False, nullable=False)
+    external = db.Column(db.String(63))
+
+    post_count = db.Column(db.Integer, default=0, nullable=False)
+    topic_count = db.Column(db.Integer, default=0, nullable=False)
 
     # One-to-one
     last_post_id = db.Column(db.Integer, db.ForeignKey("posts.id"))
@@ -432,6 +520,29 @@ class Forum(db.Model):
         """
         return "<{} {}>".format(self.__class__.__name__, self.id)
 
+    def update_last_post(self):
+        """Updates the last post. This is useful if you move a topic
+        in another forum
+        """
+        last_post = Post.query.\
+            filter(Post.topic_id == Topic.id,
+                   Topic.forum_id == self.id).\
+            order_by(Post.date_created.desc()).\
+            first()
+
+        # Last post is none when there are no topics in the forum
+        if last_post is not None:
+
+            # a new last post was found in the forum
+            if not last_post.id == self.last_post_id:
+                self.last_post_id = last_post.id
+
+        # No post found..
+        else:
+            self.last_post_id = 0
+
+        db.session.commit()
+
     def save(self, moderators=None):
         """Saves a forum"""
         if moderators is not None:
@@ -477,9 +588,9 @@ class Category(db.Model):
     __tablename__ = "categories"
 
     id = db.Column(db.Integer, primary_key=True)
-    title = db.Column(db.String)
-    description = db.Column(db.String)
-    position = db.Column(db.Integer, default=0)
+    title = db.Column(db.String(63), nullable=False)
+    description = db.Column(db.String(255))
+    position = db.Column(db.Integer, default=1, nullable=False)
 
     # One-to-many
     forums = db.relationship("Forum", backref="category", lazy="dynamic",

+ 75 - 53
flaskbb/forum/views.py

@@ -24,7 +24,8 @@ from flaskbb.utils.permissions import (can_post_reply, can_post_topic,
                                        can_move_topic)
 from flaskbb.forum.models import (Category, Forum, Topic, Post, ForumsRead,
                                   TopicsRead)
-from flaskbb.forum.forms import QuickreplyForm, ReplyForm, NewTopicForm
+from flaskbb.forum.forms import (QuickreplyForm, ReplyForm, NewTopicForm,
+                                 ReportForm)
 from flaskbb.utils.helpers import get_forums
 from flaskbb.user.models import User
 
@@ -150,55 +151,6 @@ def view_forum(forum_id):
     return render_template("forum/forum.html", forum=forum, topics=topics)
 
 
-@forum.route("/markread")
-@forum.route("/<int:forum_id>/markread")
-def markread(forum_id=None):
-
-    if not current_user.is_authenticated():
-        flash("You need to be logged in for that feature.", "danger")
-        return redirect(url_for("forum.index"))
-
-    # Mark a single forum as read
-    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()
-        TopicsRead.query.filter_by(user_id=current_user.id,
-                                   forum_id=forum.id).delete()
-
-        if not forumsread:
-            forumsread = ForumsRead()
-            forumsread.user_id = current_user.id
-            forumsread.forum_id = forum.id
-
-        forumsread.last_read = datetime.datetime.utcnow()
-        forumsread.cleared = datetime.datetime.utcnow()
-
-        db.session.add(forumsread)
-        db.session.commit()
-
-        return redirect(url_for("forum.view_forum", forum_id=forum.id))
-
-    # Mark all forums as read
-    ForumsRead.query.filter_by(user_id=current_user.id).delete()
-    TopicsRead.query.filter_by(user_id=current_user.id).delete()
-
-    forums = Forum.query.all()
-    forumsread_list = []
-    for forum in forums:
-        forumsread = ForumsRead()
-        forumsread.user_id = current_user.id
-        forumsread.forum_id = forum.id
-        forumsread.last_read = datetime.datetime.utcnow()
-        forumsread.cleared = datetime.datetime.utcnow()
-        forumsread_list.append(forumsread)
-
-    db.session.add_all(forumsread_list)
-    db.session.commit()
-
-    return redirect(url_for("forum.index"))
-
-
 @forum.route("/topic/<int:topic_id>", methods=["POST", "GET"])
 def view_topic(topic_id):
     page = request.args.get('page', 1, type=int)
@@ -320,10 +272,18 @@ def unlock_topic(topic_id):
     return redirect(url_for("forum.view_topic", topic_id=topic.id))
 
 
-@forum.route("/topic/<int:topic_id>/move")
+@forum.route("/topic/<int:topic_id>/move/<int:forum_id>")
 @login_required
-def move_topic(topic_id):
-    pass
+def move_topic(topic_id, forum_id):
+    forum = Forum.query.filter_by(id=forum_id).first_or_404()
+    topic = Topic.query.filter_by(id=topic_id).first_or_404()
+
+    if not topic.move(forum):
+        flash("Could not move the topic to forum %s" % forum.title, "danger")
+        return redirect(url_for("forum.view_topic", topic_id=topic.id))
+
+    flash("Topic was moved to forum %s" % forum.title, "success")
+    return redirect(url_for("forum.view_topic", topic_id=topic.id))
 
 
 @forum.route("/topic/<int:topic_id>/post/new", methods=["POST", "GET"])
@@ -407,6 +367,68 @@ def delete_post(post_id):
     return redirect(url_for('forum.view_topic', topic_id=topic_id))
 
 
+@forum.route("/post/<int:post_id>/report", methods=["GET", "POST"])
+@login_required
+def report_post(post_id):
+    post = Post.query.filter_by(id=post_id).first_or_404()
+
+    form = ReportForm()
+    if form.validate_on_submit():
+        form.save(current_user, post)
+        flash("Thanks for reporting!", "success")
+
+    return render_template("forum/report_post.html", form=form)
+
+
+@forum.route("/markread")
+@forum.route("/<int:forum_id>/markread")
+def markread(forum_id=None):
+
+    if not current_user.is_authenticated():
+        flash("You need to be logged in for that feature.", "danger")
+        return redirect(url_for("forum.index"))
+
+    # Mark a single forum as read
+    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()
+        TopicsRead.query.filter_by(user_id=current_user.id,
+                                   forum_id=forum.id).delete()
+
+        if not forumsread:
+            forumsread = ForumsRead()
+            forumsread.user_id = current_user.id
+            forumsread.forum_id = forum.id
+
+        forumsread.last_read = datetime.datetime.utcnow()
+        forumsread.cleared = datetime.datetime.utcnow()
+
+        db.session.add(forumsread)
+        db.session.commit()
+
+        return redirect(url_for("forum.view_forum", forum_id=forum.id))
+
+    # Mark all forums as read
+    ForumsRead.query.filter_by(user_id=current_user.id).delete()
+    TopicsRead.query.filter_by(user_id=current_user.id).delete()
+
+    forums = Forum.query.all()
+    forumsread_list = []
+    for forum in forums:
+        forumsread = ForumsRead()
+        forumsread.user_id = current_user.id
+        forumsread.forum_id = forum.id
+        forumsread.last_read = datetime.datetime.utcnow()
+        forumsread.cleared = datetime.datetime.utcnow()
+        forumsread_list.append(forumsread)
+
+    db.session.add_all(forumsread_list)
+    db.session.commit()
+
+    return redirect(url_for("forum.index"))
+
+
 @forum.route("/who_is_online")
 def who_is_online():
     if current_app.config['USE_REDIS']:

+ 1 - 0
flaskbb/static/robots.txt

@@ -0,0 +1 @@
+User-agent: *

+ 1 - 0
flaskbb/templates/admin/admin_layout.html

@@ -11,6 +11,7 @@
                 {{ navlink('admin.users', 'Users') }}
                 {{ navlink('admin.groups', 'Groups') }}
                 {{ navlink('admin.forums', 'Forums') }}
+                {{ navlink('admin.unread_reports', 'Reports') }}
             </ul>
         </div><!--/.sidebar -->
     </div><!--/.col-sm-3 -->

+ 2 - 0
flaskbb/templates/admin/forum_form.html

@@ -14,6 +14,8 @@
         {{ horizontal_field(form.category) }}
         {{ horizontal_field(form.position) }}
 
+        {{ horizontal_field(form.external) }}
+
         {{ horizontal_field(form.moderators) }}
         {{ render_boolean_field(form.show_moderators) }}
 

+ 39 - 0
flaskbb/templates/admin/reports.html

@@ -0,0 +1,39 @@
+{% extends theme("admin/admin_layout.html") %}
+{% block admin_content %}
+{% from theme('macros.html') import render_pagination %}
+
+<legend>Manage Reports | <a href="{{ url_for('admin.unread_reports') }}">Unread Reports</a></legend>
+
+<div class="pull-left" style="padding-bottom: 10px">
+    {{ render_pagination(reports, url_for('admin.reports')) }}
+</div>
+
+<table class="table table-bordered">
+    <thead>
+        <tr>
+            <th>#</th>
+            <th>Poster</th>
+            <th>Topic</th>
+            <th>Reporter</th>
+            <th>Reason</th>
+            <th>Reported</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for report in reports.items %}
+        <tr>
+            <td>{{ report.id }}</td>
+            <td>{{ report.post.user.username }}</td>
+            <td>{{ report.post.topic.title }}</td>
+            <td>{{ report.reporter.username }}</td>
+            <td>{{ report.reason }}</td>
+            <td>{{ report.reported|time_since }}</td>
+        </tr>
+        {% else %}
+        <tr>
+            <td colspan="6">No reports.</td>
+        </tr>
+        {% endfor %}
+    </tbody>
+</table>
+{% endblock %}

+ 43 - 0
flaskbb/templates/admin/unread_reports.html

@@ -0,0 +1,43 @@
+{% extends theme("admin/admin_layout.html") %}
+{% block admin_content %}
+{% from theme('macros.html') import render_pagination %}
+
+<legend>Manage Reports | <a href="{{ url_for('admin.reports') }}">All Reports</a></legend>
+
+<div class="pull-left" style="padding-bottom: 10px">
+    {{ render_pagination(reports, url_for('admin.unread_reports')) }}
+</div>
+
+<table class="table table-bordered">
+    <thead>
+        <tr>
+            <th>#</th>
+            <th>Poster</th>
+            <th>Topic</th>
+            <th>Reporter</th>
+            <th>Reason</th>
+            <th>Reported</th>
+            <th><a href="{{ url_for('admin.report_markread') }}">Mark all as Read</a></th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for report in reports.items %}
+        <tr>
+            <td>{{ report.id }}</td>
+            <td>{{ report.post.user.username }}</td>
+            <td>{{ report.post.topic.title }}</td>
+            <td>{{ report.reporter.username }}</td>
+            <td>{{ report.reason }}</td>
+            <td>{{ report.reported|time_since }}</td>
+            <td>
+                <a href="{{ url_for('admin.report_markread', report_id=report.id) }}">Mark as Read</a>
+            </td>
+        </tr>
+        {% else %}
+        <tr>
+            <td colspan="7">No unread reports.</td>
+        </tr>
+        {% endfor %}
+    </tbody>
+</table>
+{% endblock %}

+ 24 - 1
flaskbb/templates/forum/category_layout.html

@@ -19,7 +19,29 @@
         <tr>
             <td align="center" valign="center" width="4%">
 
-            {% if forum[0]|forum_is_unread(forum[1], current_user) %}
+            {% if forum[0].external %}
+                <span class="fa fa-external-link" style="font-size: 2em"></span>
+            </td>
+
+            <td valign="top">
+                <strong><a href="{{ forum[0].external }}">{{ forum[0].title }}</a></strong>
+
+                <div class="forum-description">
+                    {% autoescape false %}
+                    {{ forum[0].description|markup }}
+                    {% endautoescape %}
+                </div>
+            </td>
+
+            <td valign="top" align="center" style="white-space: nowrap">-</td>
+            <td valign="top" align="center" style="white-space: nowrap">-</td>
+            <td valign="top" align="right" style="white-space: nowrap">-</td>
+            <!-- End external -->
+            {% else %}
+
+            {% if forum[0].locked %}
+                <span class="fa fa-lock" style="font-size: 2em"></span>
+            {% elif forum[0]|forum_is_unread(forum[1], current_user) %}
                 <span class="fa fa-comments" style="font-size: 2em"></span>
             {% else %}
                 <span class="fa fa-comments-o" style="font-size: 2em"></span>
@@ -65,6 +87,7 @@
                 {% else %}
                 No posts
                 {% endif %}
+            {% endif %}
             </td>
         </tr>
         {% endfor %}

+ 1 - 1
flaskbb/templates/forum/forum.html

@@ -22,7 +22,7 @@
             <span class="fa fa-check"></span> Mark as Read
         </a>
 
-        {% if forum.locked %}
+        {% if forum[0].locked %}
         <span class="btn btn-primary">
             <span class="fa fa-lock"></span> Locked
         </span>

+ 16 - 0
flaskbb/templates/forum/report_post.html

@@ -0,0 +1,16 @@
+{% set page_title = "Report Post" %}
+
+{% extends theme("layout.html") %}
+{% block content %}
+{% from theme("macros.html") import render_field %}
+
+<form class="form" role="form" method="post">
+    {{ form.hidden_tag() }}
+    <h3>Report Post</h3>
+
+    {{ render_field(form.reason) }}
+
+    <button type="submit" class="btn btn-success">Report</button>
+    <a class="btn btn-info" href="#" onclick="window.close();return false;">Close</a>
+</form>
+{% endblock %}

+ 3 - 0
flaskbb/templates/forum/topic.html

@@ -152,6 +152,9 @@
                 </span>
 
                 <span class="pull-right">
+                    <a href="{{ url_for('forum.report_post', post_id=post.id) }}" onclick="window.open(this.href, 'wio_window','width=500,height=500'); return false;">
+                        Report
+                    </a> |
                     {% if current_user|edit_post(post.user_id, topic.forum) %}
                     <a href="{{ url_for('forum.edit_post', post_id=post.id) }}">Edit</a> |
                     {% endif %}

+ 23 - 23
flaskbb/user/models.py

@@ -31,23 +31,23 @@ class Group(db.Model):
     __tablename__ = "groups"
 
     id = db.Column(db.Integer, primary_key=True)
-    name = db.Column(db.String, unique=True)
+    name = db.Column(db.String(63), unique=True, nullable=False)
     description = db.Column(db.String(80))
 
     # I bet there is a nicer way for this :P
-    admin = db.Column(db.Boolean, default=False)
-    super_mod = db.Column(db.Boolean, default=False)
-    mod = db.Column(db.Boolean, default=False)
-    guest = db.Column(db.Boolean, default=False)
-    banned = db.Column(db.Boolean, default=False)
-
-    editpost = db.Column(db.Boolean, default=True)
-    deletepost = db.Column(db.Boolean, default=False)
-    deletetopic = db.Column(db.Boolean, default=False)
-    locktopic = db.Column(db.Boolean, default=True)
-    movetopic = db.Column(db.Boolean, default=True)
-    posttopic = db.Column(db.Boolean, default=True)
-    postreply = db.Column(db.Boolean, default=True)
+    admin = db.Column(db.Boolean, default=False, nullable=False)
+    super_mod = db.Column(db.Boolean, default=False, nullable=False)
+    mod = db.Column(db.Boolean, default=False, nullable=False)
+    guest = db.Column(db.Boolean, default=False, nullable=False)
+    banned = db.Column(db.Boolean, default=False, nullable=False)
+
+    editpost = db.Column(db.Boolean, default=True, nullable=False)
+    deletepost = db.Column(db.Boolean, default=False, nullable=False)
+    deletetopic = db.Column(db.Boolean, default=False, nullable=False)
+    locktopic = db.Column(db.Boolean, default=True, nullable=False)
+    movetopic = db.Column(db.Boolean, default=True, nullable=False)
+    posttopic = db.Column(db.Boolean, default=True, nullable=False)
+    postreply = db.Column(db.Boolean, default=True, nullable=False)
 
     # Methods
     def __repr__(self):
@@ -73,20 +73,20 @@ class User(db.Model, UserMixin):
     __tablename__ = "users"
 
     id = db.Column(db.Integer, primary_key=True)
-    username = db.Column(db.String, unique=True)
-    email = db.Column(db.String, unique=True)
+    username = db.Column(db.String(15), unique=True, nullable=False)
+    email = db.Column(db.String(63), unique=True, nullable=False)
     _password = db.Column('password', db.String(80), nullable=False)
     date_joined = db.Column(db.DateTime, default=datetime.utcnow())
     lastseen = db.Column(db.DateTime, default=datetime.utcnow())
     birthday = db.Column(db.DateTime)
-    gender = db.Column(db.String)
-    website = db.Column(db.String)
-    location = db.Column(db.String)
-    signature = db.Column(db.String)
-    avatar = db.Column(db.String)
+    gender = db.Column(db.String(1))
+    website = db.Column(db.String(63))
+    location = db.Column(db.String(63))
+    signature = db.Column(db.String(255))
+    avatar = db.Column(db.String(63))
     notes = db.Column(db.Text(5000))
 
-    theme = db.Column(db.String)
+    theme = db.Column(db.String(15))
 
     posts = db.relationship("Post", backref="user", lazy="dynamic")
     topics = db.relationship("Topic", backref="user", lazy="dynamic")
@@ -378,7 +378,7 @@ class PrivateMessage(db.Model):
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
     from_user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
     to_user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
-    subject = db.Column(db.String)
+    subject = db.Column(db.String(63))
     message = db.Column(db.Text)
     date_created = db.Column(db.DateTime, default=datetime.utcnow())
     trash = db.Column(db.Boolean, nullable=False, default=False)

+ 47 - 21
manage.py

@@ -10,9 +10,13 @@
     :copyright: (c) 2014 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
 """
+import sys
+
 from flask import current_app
-from flask.ext.script import Manager, Shell, Server, prompt, prompt_pass
-from flask.ext.migrate import MigrateCommand
+from sqlalchemy.exc import IntegrityError, OperationalError
+from flask.ext.script import (Manager, Shell, Server, prompt, prompt_pass,
+                              prompt_bool)
+from flask.ext.migrate import MigrateCommand, upgrade as db_upgrade
 
 from flaskbb import create_app
 from flaskbb.extensions import db
@@ -56,13 +60,21 @@ def dropdb():
 
 
 @manager.command
-def createall():
-    """Creates the database with some testing content."""
+def createall(dropdb=False, createdb=False):
+    """Creates the database with some testing content.
+    If you do not want to drop or create the db add
+    '-c' (to not create the db) and '-d' (to not drop the db)
+    """
+
+    if not dropdb:
+        app.logger.info("Dropping database...")
+        db.drop_all()
 
-    # Just for testing purposes
-    db.drop_all()
+    if not createdb:
+        app.logger.info("Creating database...")
+        db.create_all()
 
-    db.create_all()
+    app.logger.info("Creating test data...")
     create_test_data()
 
 
@@ -72,9 +84,10 @@ def createall():
 def create_admin(username, password, email):
     """Creates the admin user"""
 
-    username = prompt("Username")
-    email = prompt("A valid email address")
-    password = prompt_pass("Password")
+    if not (username and password and email):
+        username = prompt("Username")
+        email = prompt("A valid email address")
+        password = prompt_pass("Password")
 
     create_admin_user(username, email, password)
 
@@ -82,22 +95,35 @@ def create_admin(username, password, email):
 @manager.option('-u', '--username', dest='username')
 @manager.option('-p', '--password', dest='password')
 @manager.option('-e', '--email', dest='email')
-@manager.option('-d', '--dropdb', dest='dropdb', default=False)
-def initflaskbb(username, password, email, dropdb=False):
+def initflaskbb(username, password, email):
     """Initializes FlaskBB with all necessary data"""
 
-    if dropdb:
-        app.logger.info("Dropping previous database...")
-        db.drop_all()
-
-    app.logger.info("Creating tables...")
-    db.create_all()
-
     app.logger.info("Creating default groups...")
-    create_default_groups()
+    try:
+        create_default_groups()
+    except IntegrityError:
+        app.logger.error("Couldn't create the default groups because they are already exist!")
+        if prompt_bool("Do you want to recreate the database? (y/n)"):
+            db.session.rollback()
+            db.drop_all()
+            db.create_all()
+            create_default_groups()
+        else:
+            sys.exit(0)
+    except OperationalError:
+        app.logger.error("No database found.")
+        if prompt_bool("Do you want to create the database? (y/n)"):
+            db.session.rollback()
+            db.create_all()
+            create_default_groups()
+        else:
+            sys.exit(0)
 
     app.logger.info("Creating admin user...")
-    create_admin(username, password, email)
+    if username and password and email:
+        create_admin_user(username, password, email)
+    else:
+        create_admin(username, password, email)
 
     app.logger.info("Creating welcome forum...")
     create_welcome_forum()

+ 200 - 0
migrations/versions/1862dd9f586c_init.py

@@ -0,0 +1,200 @@
+"""init
+
+Revision ID: 1862dd9f586c
+Revises: None
+Create Date: 2014-02-08 14:14:13.315697
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '1862dd9f586c'
+down_revision = None
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('categories',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('title', sa.String(), nullable=False),
+    sa.Column('description', sa.String(), nullable=True),
+    sa.Column('position', sa.Integer(), nullable=False),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('groups',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(), nullable=False),
+    sa.Column('description', sa.String(length=80), nullable=True),
+    sa.Column('admin', sa.Boolean(), nullable=False),
+    sa.Column('super_mod', sa.Boolean(), nullable=False),
+    sa.Column('mod', sa.Boolean(), nullable=False),
+    sa.Column('guest', sa.Boolean(), nullable=False),
+    sa.Column('banned', sa.Boolean(), nullable=False),
+    sa.Column('editpost', sa.Boolean(), nullable=False),
+    sa.Column('deletepost', sa.Boolean(), nullable=False),
+    sa.Column('deletetopic', sa.Boolean(), nullable=False),
+    sa.Column('locktopic', sa.Boolean(), nullable=False),
+    sa.Column('movetopic', sa.Boolean(), nullable=False),
+    sa.Column('posttopic', sa.Boolean(), nullable=False),
+    sa.Column('postreply', sa.Boolean(), nullable=False),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('name')
+    )
+    op.create_table('users',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('username', sa.String(), nullable=False),
+    sa.Column('email', sa.String(), nullable=False),
+    sa.Column('password', sa.String(length=80), nullable=False),
+    sa.Column('date_joined', sa.DateTime(), nullable=True),
+    sa.Column('lastseen', sa.DateTime(), nullable=True),
+    sa.Column('birthday', sa.DateTime(), nullable=True),
+    sa.Column('gender', sa.String(), nullable=True),
+    sa.Column('website', sa.String(), nullable=True),
+    sa.Column('location', sa.String(), nullable=True),
+    sa.Column('signature', sa.String(), nullable=True),
+    sa.Column('avatar', sa.String(), nullable=True),
+    sa.Column('notes', sa.Text(length=5000), nullable=True),
+    sa.Column('theme', sa.String(), nullable=True),
+    sa.Column('post_count', sa.Integer(), nullable=True),
+    sa.Column('primary_group_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['primary_group_id'], ['groups.id'], ),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('email'),
+    sa.UniqueConstraint('username')
+    )
+    op.create_table('forumsread',
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('forum_id', sa.Integer(), nullable=False),
+    sa.Column('last_read', sa.DateTime(), nullable=True),
+    sa.Column('cleared', sa.DateTime(), nullable=True),
+    sa.ForeignKeyConstraint(['forum_id'], ['topics.id'], name='fk_forum_id', use_alter=True),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('user_id', 'forum_id')
+    )
+    op.create_table('posts',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('topic_id', sa.Integer(), nullable=False),
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('username', sa.String(), nullable=False),
+    sa.Column('content', sa.Text(), nullable=False),
+    sa.Column('date_created', sa.DateTime(), nullable=True),
+    sa.Column('date_modified', sa.DateTime(), nullable=True),
+    sa.Column('modified_by', sa.String(), nullable=True),
+    sa.ForeignKeyConstraint(['topic_id'], ['topics.id'], name='fk_topic_id', ondelete='CASCADE', use_alter=True),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('privatemessages',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('from_user_id', sa.Integer(), nullable=True),
+    sa.Column('to_user_id', sa.Integer(), nullable=True),
+    sa.Column('subject', sa.String(), nullable=True),
+    sa.Column('message', sa.Text(), nullable=True),
+    sa.Column('date_created', sa.DateTime(), nullable=True),
+    sa.Column('trash', sa.Boolean(), nullable=False),
+    sa.Column('draft', sa.Boolean(), nullable=False),
+    sa.Column('unread', sa.Boolean(), nullable=False),
+    sa.ForeignKeyConstraint(['from_user_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['to_user_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('topicsread',
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('topic_id', sa.Integer(), nullable=False),
+    sa.Column('forum_id', sa.Integer(), nullable=False),
+    sa.Column('last_read', sa.DateTime(), nullable=True),
+    sa.ForeignKeyConstraint(['forum_id'], ['forums.id'], name='fk_tr_forum_id', use_alter=True),
+    sa.ForeignKeyConstraint(['topic_id'], ['topics.id'], name='fk_topic_id', use_alter=True),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('user_id', 'topic_id', 'forum_id')
+    )
+    op.create_table('topictracker',
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('topic_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['topic_id'], ['topics.id'], name='fk_topic_id', use_alter=True),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], )
+    )
+    op.create_table('groups_users',
+    sa.Column('user_id', sa.Integer(), nullable=True),
+    sa.Column('group_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], )
+    )
+    op.create_table('moderators',
+    sa.Column('user_id', sa.Integer(), nullable=False),
+    sa.Column('forum_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['forum_id'], ['forums.id'], name='fk_forum_id', use_alter=True),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], )
+    )
+    op.create_table('reports',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('reporter_id', sa.Integer(), nullable=False),
+    sa.Column('reported', sa.DateTime(), nullable=True),
+    sa.Column('post_id', sa.Integer(), nullable=False),
+    sa.Column('zapped', sa.DateTime(), nullable=True),
+    sa.Column('zapped_by', sa.Integer(), nullable=True),
+    sa.Column('reason', sa.String(), nullable=True),
+    sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ),
+    sa.ForeignKeyConstraint(['reporter_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['zapped_by'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('forums',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('category_id', sa.Integer(), nullable=False),
+    sa.Column('title', sa.String(), nullable=False),
+    sa.Column('description', sa.String(), nullable=True),
+    sa.Column('position', sa.Integer(), nullable=False),
+    sa.Column('locked', sa.Boolean(), nullable=False),
+    sa.Column('show_moderators', sa.Boolean(), nullable=False),
+    sa.Column('external', sa.String(), nullable=True),
+    sa.Column('post_count', sa.Integer(), nullable=False),
+    sa.Column('topic_count', sa.Integer(), nullable=False),
+    sa.Column('last_post_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ),
+    sa.ForeignKeyConstraint(['last_post_id'], ['posts.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('topics',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('forum_id', sa.Integer(), nullable=False),
+    sa.Column('title', sa.String(), nullable=False),
+    sa.Column('user_id', sa.Integer(), nullable=True),
+    sa.Column('username', sa.String(), nullable=False),
+    sa.Column('date_created', sa.DateTime(), nullable=True),
+    sa.Column('last_updated', sa.DateTime(), nullable=True),
+    sa.Column('locked', sa.Boolean(), nullable=True),
+    sa.Column('important', sa.Boolean(), nullable=True),
+    sa.Column('views', sa.Integer(), nullable=True),
+    sa.Column('post_count', sa.Integer(), nullable=True),
+    sa.Column('first_post_id', sa.Integer(), nullable=True),
+    sa.Column('last_post_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['first_post_id'], ['posts.id'], ondelete='CASCADE'),
+    sa.ForeignKeyConstraint(['forum_id'], ['forums.id'], name='fk_forum_id', use_alter=True),
+    sa.ForeignKeyConstraint(['last_post_id'], ['posts.id'], ),
+    sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    ### end Alembic commands ###
+
+
+def downgrade():
+    ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('topics')
+    op.drop_table('forums')
+    op.drop_table('reports')
+    op.drop_table('moderators')
+    op.drop_table('groups_users')
+    op.drop_table('topictracker')
+    op.drop_table('topicsread')
+    op.drop_table('privatemessages')
+    op.drop_table('posts')
+    op.drop_table('forumsread')
+    op.drop_table('users')
+    op.drop_table('groups')
+    op.drop_table('categories')
+    ### end Alembic commands ###

+ 2 - 9
requirements.txt

@@ -13,20 +13,13 @@ Jinja2==2.7.2
 Mako==0.9.1
 MarkupSafe==0.18
 Pygments==1.6
-SQLAlchemy==0.9.1
+SQLAlchemy==0.9.2
 WTForms==1.0.5
 Werkzeug==0.9.4
-alembic==0.6.2
-amqp==1.4.2
-anyjson==0.3.3
-billiard==3.3.0.14
+alembic==0.6.3
 blinker==1.3
-celery==3.1.8
-ipython==1.1.0
 itsdangerous==0.23
-kombu==3.0.10
 postmarkup==1.2.0
-pytz==2013.9
 redis==2.9.1
 simplejson==3.3.2
 wsgiref==0.1.2