sh4nks 11 лет назад
Родитель
Сommit
080fefce06

+ 19 - 5
flaskbb/admin/views.py

@@ -1,11 +1,25 @@
 # -*- coding: utf-8 -*-
-from flask import Blueprint
-
+from flask import Blueprint, render_template
+from flaskbb.decorators import admin_required
 
 admin = Blueprint("admin", __name__)
 
 @admin.route("/")
-def admin_panel():
-    return "Admin Panel"
+@admin_required
+def overview():
+    return render_template("admin/overview.html")
+
+@admin.route("/users")
+@admin_required
+def manage_users():
+    pass
+
+@admin.route("/posts")
+@admin_required
+def manage_posts():
+    pass
 
-#TODO: Everything
+@admin.route("/pages")
+@admin_required
+def manage_pages():
+    pass

+ 56 - 3
flaskbb/app.py

@@ -17,7 +17,7 @@ from flask_debugtoolbar import DebugToolbarExtension
 
 # Import the user blueprint
 from flaskbb.user.views import user
-from flaskbb.user.models import User
+from flaskbb.user.models import User, Guest
 # Import the auth blueprint
 from flaskbb.auth.views import auth
 # Import the admin blueprint
@@ -29,7 +29,7 @@ from flaskbb.forum.views import forum
 from flaskbb.forum.models import *
 
 from flaskbb.extensions import db, login_manager, mail, cache #toolbar
-from flaskbb.utils import time_delta_format
+from flaskbb.helpers import time_delta_format, last_seen
 
 
 DEFAULT_BLUEPRINTS = (
@@ -98,6 +98,7 @@ def configure_extensions(app):
     # Flask-Login
     login_manager.login_view = app.config["LOGIN_VIEW"]
     login_manager.refresh_view = app.config["REAUTH_VIEW"]
+    login_manager.anonymous_user = Guest
 
     @login_manager.user_loader
     def load_user(id):
@@ -106,7 +107,7 @@ def configure_extensions(app):
         """
         return User.query.get(id)
 
-    login_manager.setup_app(app)
+    login_manager.init_app(app)
 
 
 def configure_blueprints(app, blueprints):
@@ -134,6 +135,53 @@ def configure_template_filters(app):
     def time_since(value):
         return time_delta_format(value)
 
+    @app.template_filter()
+    def is_online(user):
+        if user.lastseen >= last_seen():
+            return True
+        return False
+
+    @app.template_filter()
+    def is_current_user(user, post):
+        """
+        Check if the post is written by the user
+        """
+        return post.user_id == user.id
+
+    @app.template_filter()
+    def edit_post(user, post):
+        """
+        Check if the post can be edited by the user
+        """
+        if not user.is_authenticated():
+            return False
+        if user.permissions['super_mod'] or user.permissions['admin']:
+            return True
+        if post.user_id == user.id and user.permissions['editpost']:
+            return True
+        return False
+
+    @app.template_filter()
+    def delete_post(user, post):
+        """
+        Check if the post can be edited by the user
+        """
+        if not user.is_authenticated():
+            return False
+        if user.permissions['super_mod'] or user.permissions['admin']:
+            return True
+        if post.user_id == user.id and user.permissions['deletepost']:
+            return True
+        return False
+
+    @app.template_filter()
+    def post_reply(user):
+        if user.permissions['super_mod'] or user.permissions['admin']:
+            return True
+        if user.permissions['postreply']:
+            return True
+        return False
+
 
 def configure_before_handlers(app):
     """
@@ -150,6 +198,11 @@ def configure_before_handlers(app):
             db.session.add(current_user)
             db.session.commit()
 
+    @app.before_request
+    def get_user_permissions():
+        current_user.permissions = current_user.get_permissions()
+        current_user.moderate_all = current_user.permissions['admin'] or current_user.permissions['super_mod']
+
 
 def configure_errorhandlers(app):
     """

+ 4 - 1
flaskbb/auth/forms.py

@@ -8,6 +8,8 @@
     :copyright: (c) 2013 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
 """
+from datetime import datetime
+
 from flask.ext.wtf import Form
 from wtforms import TextField, PasswordField, BooleanField, HiddenField
 from wtforms.validators import Required, Email, EqualTo, regexp, ValidationError
@@ -60,7 +62,8 @@ class RegisterForm(Form):
     def save(self):
         user = User(username=self.username.data,
                     email=self.email.data,
-                    password=self.password.data)
+                    password=self.password.data,
+                    date_joined=datetime.utcnow())
         return user.save()
 
 

+ 2 - 0
flaskbb/configs/base.py

@@ -63,3 +63,5 @@ class BaseConfig(object):
     POSTS_PER_PAGE = 10
     TOPICS_PER_PAGE = 10
     USERS_PER_PAGE = 10
+
+    LAST_SEEN = 15

+ 4 - 4
flaskbb/decorators.py

@@ -11,7 +11,7 @@
 from threading import Thread
 from functools import wraps
 
-from flask import url_for, redirect
+from flask import abort
 from flask.ext.login import current_user
 
 
@@ -29,8 +29,8 @@ def admin_required(f):
     @wraps(f)
     def decorated(*args, **kwargs):
         if current_user.is_anonymous():
-            return redirect(url_for("frontend.index"))
-        if not current_user.is_admin():
-            return redirect(url_for("frontend.index"))
+            abort(403)
+        if not current_user.permissions['admin']:
+            abort(403)
         return f(*args, **kwargs)
     return decorated

+ 13 - 2
flaskbb/forum/models.py

@@ -11,7 +11,7 @@
 from datetime import datetime
 
 from flaskbb.extensions import db
-
+from flaskbb.helpers import DenormalizedText
 
 class Post(db.Model):
     __tablename__ = "posts"
@@ -142,7 +142,6 @@ class Topic(db.Model):
         return self
 
     def delete(self, users=None):
-        # This will delete the whole topic
         topic = Topic.query.filter_by(forum_id=self.forum_id).\
             order_by(Topic.last_post_id.desc())
 
@@ -191,6 +190,18 @@ class Forum(db.Model):
     # One-to-many
     topics = db.relationship("Topic", backref="forum", lazy="joined")
 
+    moderators = db.Column(DenormalizedText)
+
+    def is_moderator(self, user_id):
+        if user_id in self.moderators:
+            return True
+        return False
+
+    def add_moderator(self, user_id):
+        self.moderators.add(user_id)
+
+    def remove_moderator(self, user_id):
+        self.moderators.remove(user_id)
 
 
 class Category(db.Model):

+ 34 - 12
flaskbb/forum/views.py

@@ -9,13 +9,14 @@
     :copyright: (c) 2013 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
 """
-from datetime import datetime
+import datetime
 import math
 
 from flask import (Blueprint, render_template, redirect, url_for, current_app,
-                   request)
+                   request, flash)
 from flask.ext.login import login_required, current_user
 
+from flaskbb.helpers import last_seen
 from flaskbb.forum.models import Category, Forum, Topic, Post
 from flaskbb.forum.forms import QuickreplyForm, ReplyForm, NewTopicForm
 from flaskbb.user.models import User
@@ -34,11 +35,14 @@ def index():
     post_count = Post.query.count()
     newest_user = User.query.order_by(User.id.desc()).first()
 
+    online_users = User.query.filter(User.lastseen >= last_seen())
+
     return render_template("forum/index.html", categories=categories,
                            stats={'user_count': user_count,
                                   'topic_count': topic_count,
                                   'post_count': post_count,
-                                  'newest_user': newest_user.username})
+                                  'newest_user': newest_user.username,
+                                  'online_users': online_users})
 
 
 @forum.route("/category/<int:category_id>")
@@ -72,14 +76,16 @@ def view_topic(topic_id):
     topic.views += 1
     topic.save()
 
-    form = QuickreplyForm()
-    if form.validate_on_submit():
-        post = form.save(current_user, topic)
-        return view_post(post.id)
+    form = None
+    if current_user.permissions['postreply'] or current_user.can_moderate:
+        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,
                            per_page=current_app.config['POSTS_PER_PAGE'],
-                           form=form)
+                           last_seen=last_seen(), form=form)
 
 
 @forum.route("/post/<int:post_id>")
@@ -99,14 +105,18 @@ def view_post(post_id):
 @forum.route("/forum/<int:forum_id>/topic/new", methods=["POST", "GET"])
 @login_required
 def new_topic(forum_id):
-    form = NewTopicForm()
     forum = Forum.query.filter_by(id=forum_id).first()
 
+    if not (current_user.permissions['posttopic'] or current_user.can_moderate):
+        flash("You do not have the permissions to create a new topic.")
+        return redirect(url_for('forum.view_forum', forum_id=forum.id))
+
+    form = NewTopicForm()
     if form.validate_on_submit():
         topic = form.save(current_user, forum)
 
         # redirect to the new topic
-        return redirect(url_for('forum.view_topic', topic=topic.id))
+        return redirect(url_for('forum.view_topic', topic_id=topic.id))
     return render_template("forum/new_topic.html", forum=forum, form=form)
 
 
@@ -114,6 +124,10 @@ def new_topic(forum_id):
 @login_required
 def delete_topic(topic_id):
     topic = Topic.query.filter_by(id=topic_id).first()
+    if not (current_user.permissions['deletetopic'] or current_user.can_moderate):
+        flash("You do not have the permissions to delete the topic")
+        return redirect(url_for("forum.view_forum", forum=topic.forum_id))
+
     involved_users = User.query.filter(Post.topic_id == topic.id,
                                        User.id == Post.user_id).all()
     topic.delete(users=involved_users)
@@ -123,9 +137,13 @@ def delete_topic(topic_id):
 @forum.route("/topic/<int:topic_id>/post/new", methods=["POST", "GET"])
 @login_required
 def new_post(topic_id):
-    form = ReplyForm()
     topic = Topic.query.filter_by(id=topic_id).first()
 
+    if not (current_user.permissions['postreply'] or current_user.can_moderate):
+        flash("You do not have the permissions to delete the topic")
+        return redirect(url_for("forum.view_forum", forum=topic.forum_id))
+
+    form = ReplyForm()
     if form.validate_on_submit():
         post = form.save(current_user, topic)
         return view_post(post.id)
@@ -141,7 +159,7 @@ def edit_post(post_id):
     form = ReplyForm(obj=post)
     if form.validate_on_submit():
         form.populate_obj(post)
-        post.date_modified = datetime.utcnow()
+        post.date_modified = datetime.datetime.utcnow()
         post.save()
         return redirect(url_for('forum.view_topic', topic=post.topic.id))
     else:
@@ -165,6 +183,10 @@ def delete_post(post_id):
     return redirect(url_for('forum.view_topic', topic=topic_id))
 
 
+@forum.route("/who_is_online")
+def who_is_online():
+    pass
+
 @forum.route("/memberlist")
 def memberlist():
     page = request.args.get('page', 1, type=int)

+ 48 - 3
flaskbb/utils.py → flaskbb/helpers.py

@@ -9,11 +9,19 @@
     :license: BSD, see LICENSE for more details.
 """
 import random
-from datetime import datetime
+import datetime
 
+from flask import current_app
+from sqlalchemy import types
+from sqlalchemy.ext.mutable import Mutable
 from wtforms.widgets.core import Select, HTMLString, html_params
 
 
+def last_seen():
+    now = datetime.datetime.utcnow()
+    diff = now - datetime.timedelta(minutes=current_app.config['LAST_SEEN'])
+    return diff
+
 def generate_random_pass(length=8):
     return "".join(chr(random.randint(33, 126)) for i in range(length))
 
@@ -28,7 +36,7 @@ def time_delta_format(dt, default=None):
     if default is None:
         default = 'just now'
 
-    now = datetime.utcnow()
+    now = datetime.datetime.utcnow()
     diff = now - dt
 
     periods = (
@@ -54,6 +62,43 @@ def time_delta_format(dt, default=None):
     return default
 
 
+class DenormalizedText(Mutable, types.TypeDecorator):
+    """
+    Stores denormalized primary keys that can be
+    accessed as a set.
+
+    :param coerce: coercion function that ensures correct
+                   type is returned
+
+    :param separator: separator character
+
+    Source: https://github.com/imwilsonxu/fbone/blob/master/fbone/user/models.py#L13-L45
+    """
+
+    impl = types.Text
+
+    def __init__(self, coerce=int, separator=" ", **kwargs):
+
+        self.coerce = coerce
+        self.separator = separator
+
+        super(DenormalizedText, self).__init__(**kwargs)
+
+    def process_bind_param(self, value, dialect):
+        if value is not None:
+            items = [str(item).strip() for item in value]
+            value = self.separator.join(item for item in items if item)
+        return value
+
+    def process_result_value(self, value, dialect):
+        if not value:
+            return set()
+        return set(self.coerce(item) for item in value.split(self.separator))
+
+    def copy_value(self, value):
+        return set(value)
+
+
 class SelectDateWidget(object):
     """
     Renders a DateTime field with 3 selects.
@@ -70,7 +115,7 @@ class SelectDateWidget(object):
         '%Y': 'select_date_year'
     }
 
-    def __init__(self, years=range(1930, datetime.utcnow().year+1)):
+    def __init__(self, years=range(1930, datetime.datetime.utcnow().year+1)):
         super(SelectDateWidget, self).__init__()
         self.FORMAT_CHOICES['%Y'] = [(x, str(x)) for x in years]
 

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

@@ -0,0 +1,18 @@
+{% extends "layout.html" %}
+{% block content %}
+<div class="container">
+    This is the Admin Panel which is still a Work in Progress<br \><br \>
+    <div class="tabbable tabs-left">
+        <ul class="nav nav-tabs nav-left">
+            {%- from 'macros.html' import navlink with context-%}
+            {{ navlink('admin.overview', 'Overview') }}
+            {{ navlink('admin.manage_users', 'Manage Users') }}
+            {{ navlink('admin.manage_posts', 'Manage Posts') }}
+            {{ navlink('admin.manage_pages', 'Manage Pages') }}
+        </ul>
+        <div class="tab-content">
+            {% block admin_content %}{% endblock %}
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 20 - 0
flaskbb/templates/admin/edit_page.html

@@ -0,0 +1,20 @@
+{% extends "admin/admin_base.html" %}
+{% block admin_content %}
+<form action="{{ url_for('admin.edit_page', page_id=page.pid) }}" method="post" name="edit">
+    <fieldset>
+    {%- from 'macros/wtf_macros.html' import form_field -%}
+    {{ form.hidden_tag() }}
+
+    <legend>New Page</legend>
+
+    {{ form_field(form.title) }}
+
+    {{ form_field(form.position) }}
+
+    {{ form_field(form.content, rows="20", class="span10") }}
+
+    <button type="submit" class="btn">Save!</button>
+
+    </fieldset>
+</form>
+{% endblock %}

+ 36 - 0
flaskbb/templates/admin/manage_groups.html

@@ -0,0 +1,36 @@
+{% extends "admin/admin_base.html" %}
+{% block admin_content %}
+            <table class="table table-stripped table-hover">
+                <thead>
+                    <tr class="success">
+                        <th colspan="4">Global Statistics</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr>
+                        <td><b>SomeBlocks Version</b></td>
+                        <td>someblocks-20130220-git</td>
+                        <td><b>Posts</b></td>
+                        <td>50 Posts</td>
+                    </tr>
+                    <tr>
+                        <td><b>Python Version</b></td>
+                        <td>{{ py_ver }}</td>
+                        <td><b>Comments</b></td>
+                        <td>1237 Comments</td>
+                    </tr>
+                    <tr>
+                        <td><b>Flask Version</b></td>
+                        <td>0.9</td>
+                        <td><b>User</b></td>
+                        <td>456 Users</td>
+                    </tr>
+                    <tr>
+                        <td></td>
+                        <td></td>
+                        <td><b>Group</b></td>
+                        <td>10 Groups</td>
+                    </tr>
+                </tbody>
+            </table>
+{% endblock %}

+ 21 - 0
flaskbb/templates/admin/manage_info.html

@@ -0,0 +1,21 @@
+{% extends "admin/admin_base.html" %}
+{% block admin_content %}
+            <table class="table table-stripped table-hover">
+                <thead>
+                    <tr class="success">
+                        <th>Manage Pages</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr>
+                        <td colspan="2">
+                            <b><a href="#">Page Title</a></b>
+                        </td>
+                        <td colspan="2">
+                            <i class="icon-trash"></i> <a href="#">Edit Page</a>
+                            <i class="icon-edit"></i> <a href="#">Delete Page</a>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+{% endblock %}

+ 27 - 0
flaskbb/templates/admin/manage_pages.html

@@ -0,0 +1,27 @@
+{% extends "admin/admin_base.html" %}
+{% block admin_content %}
+            <table class="table table-stripped table-hover">
+                <thead>
+                    <tr class="success">
+                        <th>Manage Pages | <small><a href="{{ url_for('admin.new_page') }}">New Page</a><small></th>
+                    </tr>
+                </thead>
+                <tbody>
+                {% for page in pages %}
+                    <tr>
+                        <td colspan="2">
+                            {% if not page.external %}
+                            <b><a href="{{ url_for('frontend.pages', url=page.url) }}">{{ page.title }}</a></b>
+                            {% else %}
+                            <b><a href="{{ page.url }}">{{ page.title }}</a></b> <i class="icon-external-link"></i>
+                            {% endif %}
+                        </td>
+                        <td colspan="2">
+                            <i class="icon-trash"></i> <a href="{{ url_for('admin.edit_page', page_id=page.pid) }}">Edit Page</a>
+                            <i class="icon-edit"></i> <a href="{{ url_for('admin.delete_page', page_id=page.pid) }}">Delete Page</a>
+                        </td>
+                    </tr>
+                {% endfor %}
+                </tbody>
+            </table>
+{% endblock %}

+ 23 - 0
flaskbb/templates/admin/manage_posts.html

@@ -0,0 +1,23 @@
+{% extends "admin/admin_base.html" %}
+{% block admin_content %}
+            <table class="table table-stripped table-hover">
+                <thead>
+                    <tr class="success">
+                        <th>Manage Posts | <small><a href="{{ url_for('blog.new_post') }}">New Post</a><small></th>
+                    </tr>
+                </thead>
+                <tbody>
+                {% for post in posts %}
+                    <tr>
+                        <td colspan="2">
+                            <b><a href="{{ url_for('blog.post', id=post.pid) }}">{{ post.title }}</a></b>
+                        </td>
+                        <td colspan="2">
+                            <i class="icon-trash"></i> <a href="{{ url_for('blog.edit_post', id = post.pid) }}">Edit Post</a>
+                            <i class="icon-edit"></i> <a href="{{ url_for('blog.delete_post', id = post.pid) }}">Delete Post</a>
+                        </td>
+                    </tr>
+                {% endfor %}
+                </tbody>
+            </table>
+{% endblock %}

+ 23 - 0
flaskbb/templates/admin/manage_users.html

@@ -0,0 +1,23 @@
+{% extends "admin/admin_base.html" %}
+{% block admin_content %}
+            <table class="table table-stripped table-hover">
+                <thead>
+                    <tr class="success">
+                        <th>Manage Users</th>
+                    </tr>
+                </thead>
+                <tbody>
+                {% for user in users %}
+                    <tr>
+                        <td colspan="2">
+                            <b><a href="{{ url_for('users.profile', username=user.username) }}">{{ user.username }}</a></b>
+                        </td>
+                        <td colspan="2">
+                            <i class="icon-trash"></i> <a href="{{ url_for('users.editprofile', username = user.username) }}">Edit User</a>
+                            <i class="icon-edit"></i> <a href="{{ url_for('admin.delete_user', username = user.username) }}">Delete User</a>
+                        </td>
+                    </tr>
+                {% endfor %}
+                </tbody>
+            </table>
+{% endblock %}

+ 25 - 0
flaskbb/templates/admin/new_page.html

@@ -0,0 +1,25 @@
+{% extends "admin/admin_base.html" %}
+{% block admin_content %}
+<form action="{{ url_for('admin.new_page') }}" method="post" name="new">
+    <fieldset>
+    {%- from 'macros/wtf_macros.html' import form_field -%}
+
+    {{ form.hidden_tag() }}
+
+    <legend>New Page</legend>
+
+    {{ form_field(form.title) }}
+
+    {{ form_field(form.url, placeholder="e.q. about or about_us") }}
+
+    {{ form_field(form.external) }}
+
+    {{ form_field(form.position) }}
+
+    {{ form_field(form.content, rows="20", class="span10") }}
+
+
+    <button type="submit" class="btn">Submit!</button>
+    </fieldset>
+</form>
+{% endblock %}

+ 32 - 0
flaskbb/templates/admin/overview.html

@@ -0,0 +1,32 @@
+{% extends "admin/admin_layout.html" %}
+{% block admin_content %}
+
+            <table class="table table-stripped table-hover">
+                <thead>
+                    <tr class="success">
+                        <th colspan="4">Global Statistics</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr>
+                        <td><b>FlaskBB Version</b></td>
+                        <td>0.1</td>
+                        <td><b>Posts</b></td>
+                        <td>10 Posts</td>
+                    </tr>
+                    <tr>
+                        <td><b>Python Version</b></td>
+                        <td>2.7.5</td>
+                        <td><b>Comments</b></td>
+                        <td>10 Comments</td>
+                    </tr>
+                    <tr>
+                        <td><b>Flask Version</b></td>
+                        <td>0.10.1</td>
+                        <td><b>User</b></td>
+                        <td>10 Users</td>
+                    </tr>
+                </tbody>
+            </table>
+
+{% endblock %}

+ 4 - 3
flaskbb/templates/forum/category_layout.html

@@ -16,15 +16,16 @@
 
         {% for forum in category.forums %}
         <tr>
-            <td align="center" valign="center" width="1">INSERT IMAGE</td>
+            <td align="center" valign="center" width="1">
+                New </br> Posts
+            </td>
 
             <td valign="top">
                 <strong><a href="{{ url_for('forum.view_forum', forum_id=forum.id) }}">{{ forum.title }}</a></strong>
 
                 <div class="forum-description">
                     {{ forum.description }}<br />
-                    <!-- Moderators: <a href="#">FlaskBB Team</a><br />
-
+                    <!--
                     <strong>Sub Forums:</strong> <a href="#" title="">Subforum 1</a>, <a href="#" title="">Subforum 2</a>
                      -->
                 </div>

+ 2 - 11
flaskbb/templates/forum/index.html

@@ -14,6 +14,7 @@
         <tr>
             <td colspan="2">
                 <strong>Board Statistics</strong>
+                [<a href="{{ url_for('forum.who_is_online') }}" onclick="window.open(this.href, 'wio_window','width=500,height=500'); return false;">Who is online?</a>]
             </td>
         </tr>
     </thead>
@@ -26,21 +27,11 @@
             </td>
             <td>
                 Newest registered user: <a href="{{ url_for('user.profile', username=stats['newest_user']) }}">{{ stats['newest_user'] }}</a> <br />
-                <strong>#TODO</strong> Registered users online: <strong>2</strong> <br />
+                Registered users online: <strong>{{ stats['online_users'].count() }}</strong> <br />
                 <strong>#TODO</strong> Guests online: <strong>40</strong> <br />
             </td>
         </tr>
 
-        <tr>
-            <td colspan="2"><strong>#TODO</strong> Who is online? [<a href="show_all_online_players.html" target="_blank">Full List</a>]</td>
-        </tr>
-        <tr>
-            <td colspan="2">
-                <a href="profile.html"><span style="color: green;"><strong><em>test1</em></strong></span></a>,
-                <a href="profile.html"><span style=""><em>test2</em></span></a>
-            </td>
-        </tr>
-
     </tbody>
 </table>
 

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

@@ -9,7 +9,6 @@
     <li class="active">Memberlist</li>
 </ul>
 
-
   <div class="pull-left" style="padding-bottom: 10px">
     {{ render_pagination(users, url_for('forum.memberlist')) }}
   </div><!-- /.col-lg-6 -->
@@ -39,7 +38,7 @@
             <td><a href="{{ url_for('user.profile', username=user.username) }}">{{ user.username }}</a></td>
             <td>{{ user.post_count }}</td>
             <td>{{ user.date_joined|format_date('%b %d %Y') }}</td>
-            <td>#TODO</td>
+            <td>{{ user.primary_group.name }}</td>
         </tr>
         {% endfor %}
     </tbody>

+ 19 - 16
flaskbb/templates/forum/topic.html

@@ -16,14 +16,14 @@
     {{ render_pagination(posts, url_for('forum.view_topic', topic_id=topic.id)) }}
 </div> <!-- end span pagination -->
 
-{% if current_user.is_authenticated() %}
 <div class="pull-right" style="padding-bottom: 10px">
+    {% if current_user|post_reply() %}
     <a href="{{ url_for('forum.new_post', topic_id=topic.id) }}" class="btn btn-primary">Reply</a>
-    {% if current_user.id == topic.user_id %}
+    {% endif %}
+    {% if current_user|delete_topic(topic) %}
     <a href="{{ url_for('forum.delete_topic', topic_id=topic.id) }}" class="btn btn-primary">Delete Topic</a>
     {% endif %}
 </div>
-{% endif %}
 
 <table class="table table-bordered">
     <tbody>
@@ -54,23 +54,25 @@
         <td>
             <table class="table table-borderless">
                 <tr>
-
+                    {% if post.user.avatar %}
                     <td width="1">
                         <img src="{{ post.user.avatar }}" alt="Avatar" height="100" width="100">
                     </td>
-
+                    {% endif %}
                     <td>
                         <a href="{{ url_for('user.profile', username=post.user.username) }}"><span style="color: green;"><strong><em>{{ post.user.username }}</em></strong></span></a>
-                        <!-- TODO: Implement online status and groups -->
-                        <span class="label label-success">Online</span><br />
-                        Administrator<br />
+                        {% if post.user|is_online %}
+                        <span class="label label-success">Online</span>
+                        {% else %}
+                        <span class="label label-default">Offline</span>
+                        {% endif %}
+                        <br />
+                        {{ post.user.primary_group.name }}<br />
                     </td>
 
                     <td class="pull-right">
                         Posts: {{ post.user.post_count }}<br />
                         Registered since: {{ post.user.date_joined|format_date('%b %d %Y') }}<br />
-                        <!-- TODO: Implement Karma functionality -->
-                        Karma: <a href="#">124</a>
                     </td>
 
                 </tr>
@@ -94,17 +96,18 @@
         <tr>
             <td>
                 <span class="pull-left">
-                    <a href="{{ url_for('pms.new_message') }}?to_user={{ post.user.username }}">PM</a> | <a href="#">Website</a>
+                    <a href="{{ url_for('pms.new_message') }}?to_user={{ post.user.username }}">PM</a>
+                    {% if post.user.website %}| <a href="{{post.user.website}}">Website</a>{% endif %}
                 </span>
 
                 <span class="pull-right">
-                    {% if current_user.is_authenticated() and current_user.id == post.user_id %}
+                    {% if current_user|edit_post(post) %}
                     <a href="{{ url_for('forum.edit_post', post_id=post.id) }}">Edit</a> |
+                    {% endif %}
+                    {% if current_user|delete_post(post) %}
                     <a href="{{ url_for('forum.delete_post', post_id=post.id) }}">Delete</a> |
                     {% endif %}
-                    <a href="#">Quote</a> |
-                    <a href="#">Report</a> |
-                    <a href="#">+1</a>
+                    <a href="#">Quote</a>
                 </span>
             </td>
         </tr>
@@ -113,7 +116,7 @@
     </tbody>
 </table>
 
-{% if current_user.is_authenticated() %}
+{% if current_user|post_reply() %}
 <form class="form" action="#" method="post">
     {{ form.hidden_tag() }}
     <div class="form-group">

+ 7 - 0
flaskbb/templates/layout.html

@@ -48,6 +48,13 @@
                             </button>
                             <ul class="dropdown-menu" role="menu">
                                 <li><a href="{{ url_for('user.settings') }}"><span class="glyphicon glyphicon-cog"></span> Settings</a></li>
+
+                                {% if current_user.permissions['admin'] %}
+                                <li class="divider"></li>
+                                <li><a href="{{ url_for('admin.overview') }}"><span class="glyphicon glyphicon-cog"></span> Admin Panel</a></li>
+                                <li class="divider"></li>
+                                {% endif %}
+
                                 <li><a href="{{ url_for('auth.logout') }}"><span class="glyphicon glyphicon-share-alt"></span> Logout</a></li>
                             </ul>
                         </div>

+ 3 - 1
flaskbb/templates/pms/view_message.html

@@ -7,15 +7,17 @@
             <table class="table table-borderless" border="0">
                 <tr>
 
+                    {% if message.from_user.avatar %}
                     <td width="1">
                         <img src="{{ message.from_user.avatar }}" alt="Avatar" height="100" width="100">
                     </td>
+                    {% endif %}
 
                     <td>
                         <a href="{{ url_for('user.profile', username=message.from_user.username) }}"><span style="color: green;"><strong><em>{{ message.from_user.username }}</em></strong></span></a>
                         <!-- TODO: Implement online status and groups -->
                         <span class="badge badge-success">Online</span><br />
-                        Administrator<br />
+                        {{ message.from_user.primary_group.name }}<br />
                     </td>
 
                     <td class="pull-right">

+ 14 - 2
flaskbb/templates/user/profile.html

@@ -13,10 +13,13 @@
     </thead>
     <tbody>
         <tr>
+
           <td width="200px">
             <table class="table table-borderless">
               <tbody>
+                {% if user.avatar %}
                 <tr><td><img src="{{ user.avatar }}" alt="Avatar" height="100" width="100"> </td></tr>
+                {% endif %}
                 <!-- TODO: Implement online status -->
                 <tr><td><b></b> <span class="label label-success">Online</span></td></tr>
                 <tr><td><a href="{{ url_for('user.view_all_topics', username=user.username) }}">All Topics</a></td></tr>
@@ -24,13 +27,21 @@
               </tbody>
             </table>
           </td>
-          <td>{{ user.notes }}</td>
+
+          <td>
+            {% if user.notes %}
+                {{ user.notes }}
+            {% else %}
+                User has not added any notes about him.
+            {% endif %}
+          </td>
+
           <td width="250px">
             <table class="table table-borderless">
               <tbody>
                 <tr>
                   <td align="right">Group:</td>
-                  <td><b>TODO: </b>Admin</td>
+                  <td>{{ user.primary_group.name }}</td>
                 </tr>
                 <tr>
                   <td align="right">Joined:</td>
@@ -68,6 +79,7 @@
               </tbody>
             </table>
           </td>
+
         </tr>
     </tbody>
 </table>

+ 1 - 1
flaskbb/user/forms.py

@@ -16,7 +16,7 @@ from wtforms.validators import (Length, Required, Email, EqualTo, regexp,
 
 from flaskbb.user.models import User
 from flaskbb.extensions import db
-from flaskbb.utils import SelectDateWidget
+from flaskbb.helpers import SelectDateWidget
 
 IMG_RE = r'^[^/\\]\.(?:jpg|gif|png)'
 

+ 189 - 4
flaskbb/user/models.py

@@ -11,15 +11,45 @@
 from datetime import datetime
 
 from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
-from itsdangerous import SignatureExpired, BadSignature
+from itsdangerous import SignatureExpired
 from werkzeug import generate_password_hash, check_password_hash
 from flask import current_app
-from flask.ext.login import UserMixin
-
-from flaskbb.extensions import db
+from flask.ext.login import UserMixin, AnonymousUserMixin
+from flaskbb.extensions import db, cache
 from flaskbb.forum.models import Post, Topic
 
 
+groups_users = db.Table('groups_users',
+        db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
+        db.Column('group_id', db.Integer(), db.ForeignKey('groups.id')))
+
+moderators = db.Table('moderators',
+        db.Column('forum_id', db.Integer(), db.ForeignKey('forums.id')),
+        db.Column('user_id', db.Integer(), db.ForeignKey('users.id')))
+
+
+class Group(db.Model):
+    __tablename__ = "groups"
+
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.String, unique=True)
+    description = db.Column(db.String(80))
+
+    admin = db.Column(db.Boolean)
+    super_mod = db.Column(db.Boolean)
+    mod = db.Column(db.Boolean)
+    guest = db.Column(db.Boolean)
+    banned = db.Column(db.Boolean)
+
+    editpost = db.Column(db.Boolean)
+    deletepost = db.Column(db.Boolean)
+    deletetopic = db.Column(db.Boolean)
+    posttopic = db.Column(db.Boolean)
+    postreply = db.Column(db.Boolean)
+    viewtopic = db.Column(db.Boolean)
+    viewprofile = db.Column(db.Boolean)
+
+
 class User(db.Model, UserMixin):
     __tablename__ = "users"
 
@@ -43,6 +73,16 @@ class User(db.Model, UserMixin):
 
     post_count = db.Column(db.Integer, default=0) # Bye bye normalization
 
+    primary_group_id = db.Column(db.Integer, db.ForeignKey('groups.id'))
+
+    primary_group = db.relationship('Group', backref="user_group", uselist=False, foreign_keys=[primary_group_id])
+
+    groups = db.relationship('Group',
+                             secondary=groups_users,
+                             primaryjoin=(groups_users.c.user_id == id),
+                             backref=db.backref('users', lazy='dynamic'),
+                             lazy='dynamic')
+
     def __repr__(self):
         return "Username: %s" % self.username
 
@@ -130,6 +170,85 @@ class User(db.Model, UserMixin):
         return Post.query.filter(Post.user_id == self.id).\
             paginate(page, current_app.config['TOPICS_PER_PAGE'], False)
 
+    def add_to_group(self, group):
+        """
+        Adds the user to the `group` if he isn't in it.
+        """
+        if not self.in_group(group):
+            self.groups.append(group)
+            return self
+
+    def in_group(self, group):
+        """
+        Returns True if the user is in the specified group
+        """
+        return self.groups.filter(
+            groups_users.c.group_id == group.id).count() > 0
+
+    @cache.memoize(60*5)
+    def get_permissions(self, exclude=None):
+        """
+        Returns a dictionary with all the permissions the user has.
+        """
+        exclude = exclude or []
+        exclude.extend(['id', 'name', 'description'])
+
+        perms = {}
+        # Iterate over all groups
+        for group in self.groups.all():
+            for c in group.__table__.columns:
+                # try if the permission already exists in the dictionary
+                # and if the permission is true, go to the next permission
+                try:
+                    if perms[c.name]:
+                        continue
+                # if the permission doesn't exist in the dictionary
+                # add it to the dictionary
+                except KeyError:
+                    # if the permission is in the exclude list,
+                    # skip to the next permission
+                    if c.name in exclude:
+                        continue
+                    perms[c.name] = getattr(group, c.name)
+        return perms
+
+    def has_perm(self, perm):
+        """
+        Returns True if the user has the specified permission.
+        """
+        permissions = self.get_permissions()
+        if permissions['admin'] or permissions['super_mod']:
+            return True
+
+        if permissions[perm]:
+            return True
+        return False
+
+    def has_one_perm(self, perms_list):
+        """
+        Returns True if the user has one of the provided permissions.
+        """
+        for perm in perms_list:
+            if self.has_perm(perm):
+                return True
+        return False
+
+    def has_perms(self, perms_list):
+        """
+        Returns True if the user has each of the specified permissions.
+        It is basically the same as has_perm but every permission in the
+        provided list needs to be True to return True.
+        """
+        # Iterate over the list with the permissions
+        for perm in perms_list:
+            if self.has_perm(perm):
+                # After checking the permission,
+                # we can remove the perm from the list
+                perms_list.remove(perm)
+            else:
+                return False
+        return True
+
     def save(self):
         db.session.add(self)
         db.session.commit()
@@ -139,3 +258,69 @@ class User(db.Model, UserMixin):
         db.session.delete(self)
         db.session.commit()
         return self
+
+
+class Guest(AnonymousUserMixin):
+    @cache.memoize(60*5)
+    def get_permissions(self, exclude=None):
+        """
+        Returns a dictionary with all permissions the user has
+        """
+        exclude = exclude or []
+        exclude.extend(['id', 'name', 'description'])
+
+        perms = {}
+        # Get the Guest group
+        group = Group.query.filter_by(guest=True).first()
+        for c in group.__table__.columns:
+            # try if the permission already exists in the dictionary
+            # and if the permission is true, go to the next permission
+            try:
+                if perms[c.name]:
+                    continue
+            # if the permission doesn't exist in the dictionary
+            # add it to the dictionary
+            except KeyError:
+                # if the permission is in the exclude list,
+                # skip to the next permission
+                if c.name in exclude:
+                    continue
+                perms[c.name] = getattr(group, c.name)
+        return perms
+
+    def has_perm(self, perm):
+        """
+        Returns True if the user has the specified permission.
+        """
+        group = Group.query.filter_by(guest=True).first()
+        if getattr(group, perm, True):
+            return True
+        return False
+
+    def has_one_perm(self, perms_list):
+        """
+        Returns True if the user has one of the provided permissions.
+        """
+        group = Group.query.filter_by(guest=True).first()
+        for perm in perms_list:
+            if getattr(group, perm, True):
+                return True
+        return False
+
+    def has_perms(self, perms_list):
+        """
+        Returns True if the user has each of the specified permissions.
+        It is basically the same as has_perm but every permission in the
+        provided list needs to be True to return True.
+        """
+        # Iterate overall groups
+        group = Group.query.filter_by(guest=True).first()
+        # Iterate over the list with the permissions
+        for perm in perms_list:
+            if getattr(group, perm, True):
+                # After checking the permission,
+                # we can remove the perm from the list
+                perms_list.remove(perm)
+            else:
+                return False
+        return True

+ 108 - 1
manage.py

@@ -11,6 +11,7 @@
     :license: BSD, see LICENSE for more details.
 """
 import os
+from collections import OrderedDict
 
 from flask import current_app
 from flask.ext.script import Manager, Shell, Server
@@ -19,7 +20,7 @@ from flaskbb import create_app
 from flaskbb.configs.development import DevelopmentConfig, BaseConfig
 from flaskbb.extensions import db
 
-from flaskbb.user.models import User
+from flaskbb.user.models import User, Group
 from flaskbb.forum.models import Post, Topic, Forum, Category
 
 app = create_app(DevelopmentConfig)
@@ -58,11 +59,117 @@ def createall():
 
     db.create_all()
 
+    groups = OrderedDict((
+        ('Administrator', {
+             'description': 'The Administrator Group',
+             'admin': True,
+             'super_mod': False,
+             'mod': False,
+             'banned': False,
+             'guest': False,
+             'editpost': True,
+             'deletepost': True,
+             'deletetopic': True,
+             'posttopic': True,
+             'postreply': True,
+             'viewtopic': True,
+             'viewprofile': True
+             }),
+        ('Super Moderator', {
+             'description': 'The Super Moderator Group',
+             'admin': False,
+             'super_mod': True,
+             'mod': False,
+             'banned': False,
+             'guest': False,
+             'editpost': True,
+             'deletepost': True,
+             'deletetopic': True,
+             'posttopic': True,
+             'postreply': True,
+             'viewtopic': True,
+             'viewprofiles': True
+             }),
+        ('Moderator', {
+             'description': 'The Moderator Group',
+             'admin': False,
+             'super_mod': False,
+             'mod': True,
+             'banned': False,
+             'guest': False,
+             'editpost': True,
+             'deletepost': True,
+             'deletetopic': True,
+             'posttopic': True,
+             'postreply': True,
+             'viewtopic': True,
+             'viewprofile': True
+             }),
+        ('Member', {
+             'description': 'The Member Group',
+             'admin': False,
+             'super_mod': False,
+             'mod': False,
+             'banned': False,
+             'guest': False,
+             'editpost': True,
+             'deletepost': False,
+             'deletetopic': False,
+             'posttopic': True,
+             'postreply': True,
+             'viewtopic': True,
+             'viewprofile': True
+             }),
+        ('Banned', {
+             'description': 'The Banned Group',
+             'admin': False,
+             'super_mod': False,
+             'mod': False,
+             'banned': True,
+             'guest': False,
+             'editpost': False,
+             'deletepost': False,
+             'deletetopic': False,
+             'posttopic': False,
+             'postreply': False,
+             'viewtopic': False,
+             'viewprofile': False
+             }),
+        ('Guest', {
+             'description': 'The Guest Group',
+             'admin': False,
+             'super_mod': False,
+             'mod': False,
+             'banned': False,
+             'guest': True,
+             'editpost': False,
+             'deletepost': False,
+             'deletetopic': False,
+             'posttopic': False,
+             'postreply': False,
+             'viewtopic': False,
+             'viewprofile': False
+             })
+    ))
+
+    # create 5 groups
+    for key, value in groups.items():
+        group = Group(name=key)
+
+        for k, v in value.items():
+            setattr(group, k, v)
+
+        db.session.add(group)
+        db.session.commit()
+
     # create 5 users
+    groups = Group.query.all()
     for u in range(1, 6):
         username = "test%s" % u
         email = "test%s@example.org" % u
         user = User(username=username, password="test", email=email)
+        user.groups.append(groups[u-1])
+        user.primary_group_id = u
         db.session.add(user)
 
     # create 2 categories