Commits

Josh VanderLinden committed 039a26b

Adjusted the permissions considerably. Now there is a permission matrix that resembles the work I originally did on VCBoard several years ago. It does take a toll on performance, but there is a bit of caching to offset the load. I changed the decorator to respect the permissions as defined by the permissions matrix view. Added a form for users to be able to reply to a thread. Added the form that handles the permission matrix. Updated the listeners a bit. Fixed the permissions in the models.py so they're all in one place and arranged properly. Created a UserGroup class that extends the base auth.models.Group class so I can keep track of other pertinent information about user groups. Added some methods to return the last post information in forums and threads. Fixed the ranks a bit. Updated several templates to respect permissions as specified in the permission matrix. Added several views that were missing.

Comments (0)

Files changed (21)

vcboard/__init__.py

 from models import SettingManager
+
 config = SettingManager()
+

vcboard/admin_views.py

+from django.contrib.admin.views.decorators import staff_member_required
+from django.contrib.auth.models import User, Permission
+from django.contrib.sites.models import Site
+from django.core.cache import cache
+from django.http import Http404
+from django.shortcuts import get_object_or_404
+from django.utils.translation import ugettext_lazy as _
+from vcboard.forms import PermissionMatrixForm
+from vcboard.models import Forum, UserGroup, ForumPermission, GroupPermission, UserPermission
+from vcboard.ranks.models import Rank, RankPermission
+from vcboard.utils import render
+import re
+
+@staff_member_required
+def permission_matrix(request, obj_type, id, template='admin/permission_matrix.html'):
+    """
+    Allows the user to quickly adjust permissions for forums, groups, ranks
+    and individual users
+    """
+
+    klass, matrix_type = {
+        'forum': (Forum, ForumPermission),
+        'group': (UserGroup, GroupPermission),
+        'rank': (Rank, RankPermission),
+        'user': (User, UserPermission),
+    }.get(obj_type, None)
+
+    # only take the classes we want
+    if not klass:
+        raise Http404
+
+    obj = get_object_or_404(klass, pk=id)
+    permissions = matrix_type.objects.filter(**{str(obj_type + '__id'): obj.id})
+
+    # make the forum permission matrix view slightly different
+    forum = None
+    if isinstance(obj, Forum):
+        forum = obj
+    
+    if request.method == 'POST':
+        form = PermissionMatrixForm(request.POST, forum=forum)
+        if form.is_valid():
+            forums = {}
+            perms = {}
+            site = Site.objects.get_current()
+
+            for field_name in form.fields.keys():
+                forum_id, permission_id = re.findall('f_(\d+)_p_(\d+)', field_name)[0]
+
+                # retrieve the forum
+                f = forums.get(forum_id, None)
+                if not f:
+                    f = Forum.objects.get(pk=forum_id)
+                    forums[forum_id] = f
+
+                # retrieve the permission
+                p = perms.get(permission_id, None)
+                if not p:
+                    p = Permission.objects.get(pk=permission_id)
+                    perms[permission_id] = p
+
+                params = {'site': site, 'forum': f, 'permission': p}
+                if not forum:
+                    params[str(obj_type)] = obj
+
+                perm, c = matrix_type.objects.get_or_create(**params)
+                value = form.cleaned_data[field_name]
+                if perm.has_permission != value:
+                    perm.has_permission = value
+                    perm.save()
+
+            # remove previously cached permissions for this forum.  This makes
+            # it so the permissions have to be refreshed
+            perms = cache.get('perms_for_forums', {})
+            cached_perm_dict = cache.get('vcboard_user_perms', {})
+            for forum_id in forums.keys():
+                if perms.has_key(forum_id):
+                    for key in perms[forum_id]:
+                        del cached_perm_dict[key]
+                perms[str(forum_id)] = []
+            cache.set('vcboard_user_perms', cached_perm_dict)
+            cache.set('perms_for_forums', perms)
+
+            request.user.message_set.create(message='Permissions have been saved.')
+    else:
+        form = PermissionMatrixForm(permissions=permissions, forum=forum)
+
+    data = {
+        'object': obj,
+        'form': form
+    }
+
+    return render(request, template, data)

vcboard/decorators.py

 from django.http import HttpResponseRedirect
 from models import Setting, Forum, Thread
 from vcboard import config
-from utils import can
+from utils import get_user_permissions
 from functools import wraps
 
 LOGIN_URL = settings.LOGIN_URL
     return HttpResponseRedirect('%s?%s=%s' % (LOGIN_URL, REDIRECT_FIELD_NAME, 
                                               request.path))
 
-def permission_required(permission, forum=None, thread=None):
+def permission_required(permission):
     def wrap(func):
         @wraps(func)
         def wrapped(request, *args, **kwargs):
-            f = forum and Forum.objects.with_path(kwargs['path']) or None
-            t = thread and Thread.objects.get(pk=kwargs['thread_id'], forum=f) or None
-            if can(request.user, permission, forum=f):
+            try:
+                forum = kwargs.get('forum', args[0])
+            except IndexError:
+                forum = None
+
+            if forum:
+                perms = get_user_permissions(request.user, forum)
+                if perms.get(permission, False):
+                    return func(request, *args, **kwargs)
+                else:
+                    return redirect_to_login(request)
+            else:
                 return func(request, *args, **kwargs)
-            else:
-                return redirect_to_login(request)
         return wrapped
     return wrap
 from django import forms
-from models import Thread, Post
+from django.contrib.auth.models import Permission
+from models import Forum, Thread, Post
 
 class ThreadForm(forms.ModelForm):
     class Meta:
         model = Thread
         fields = ('subject', 'content', 'is_draft')
+
+class ReplyForm(forms.ModelForm):
+    class Meta:
+        model = Post
+        fields = ('subject', 'content', 'is_draft')
+
+class PermissionMatrixForm(forms.Form):
+    permissions = []
+    forums = []
+
+    def __init__(self, *args, **kwargs):
+        permissions = kwargs.pop('permissions', [])
+        forum = kwargs.pop('forum', None)
+
+        super(PermissionMatrixForm, self).__init__(*args, **kwargs)
+        self.all_permissions = Permission.objects.filter(codename__startswith='vcb_')
+        self.permissions = list(self.all_permissions)
+
+        self.values = dict(('f_%i_p_%i' % (p.forum.id, p.permission.id), p.has_permission) for p in permissions)
+
+        if forum:
+            self.add_forum(forum)
+        else:
+            # retrieve all forums that are not categories
+            for forum in Forum.objects.active(): #.filter(is_category=False):
+                self.add_forum(forum)
+
+        try:
+            self.data = kwargs.get('data', args[0])
+        except:
+            # we don't need data *that* bad :)
+            pass
+
+    def add_forum(self, forum):
+        if forum not in self.forums:
+            self.forums.append(forum)
+        for perm in self.all_permissions:
+            key = 'f_%i_p_%i' % (forum.id, perm.id)
+            self.fields[key] = forms.BooleanField(required=False, initial=self.values.get(key, False))

vcboard/listeners.py

 from django.db.models import signals
 from vcboard import config, signals as vcb
-from models import Setting, Forum, Thread, Post
+from models import Setting, Forum, Thread, Post, UserGroup
 from datetime import datetime
 
+def only_one_default_group(sender, instance, created, **kwargs):
+    """
+    Makes sure there is only one default group at any time
+    """
+    UserGroup.objects.exclude(pk=instance.pk).update(is_default=False)
+
 def update_config(sender, instance, created, **kwargs):
     """
     Updates the configuration cache with new settings
             instance.parent.reply_count += 1
             instance.parent.last_post = instance
             instance.parent.save()
+            collection = instance.parent.forum.hierarchy
+        else:
+            collection = instance.forum.hierarchy
 
-        for forum in instance.forum.hierarchy:
+        for forum in collection:
             if sender == Thread:
                 forum.thread_count += 1
 
 signals.post_save.connect(update_config, sender=Setting)
 vcb.object_shown.connect(update_last_in, sender=Forum)
 vcb.object_shown.connect(update_last_in, sender=Thread)
+signals.post_save.connect(only_one_default_group, sender=UserGroup)
+

vcboard/models.py

 from django.db import models
-from django.contrib.auth.models import User, AnonymousUser
+from django.contrib.auth.models import User, AnonymousUser, Group, Permission
 from django.contrib.sites.models import Site
 from django.core.urlresolvers import reverse
-from django.template.defaultfilters import mark_safe
+from django.template.defaultfilters import mark_safe, timesince
 from django.utils.translation import ugettext_lazy as _
-from utils import unique_slug
+from utils import unique_slug, PP
 
 class SettingManager(models.Manager):
     _cache = {}
 
     class Meta:
         ordering = ('section', 'key')
+
+        # yes, some of these overlap with the built-in permissions. oh well.
         permissions = (
-            (_('Can view forum home'), 'view_forum_home'),
-            (_('Can view forum'), 'view_forum'),
+            (PP('view_forum'), 'Can view forum'),
+            (PP('view_other_threads'), 'Can view threads started by others'),
+            (PP('edit_own_threads'), 'Can edit own threads'),
+            (PP('edit_other_threads'), 'Can edit threads started by others'),
+            (PP('close_own_threads'), 'Can close own threads'),
+            (PP('close_other_threads'), 'Can close threads started by others'),
+            (PP('open_own_threads'), 'Can open own threads'),
+            (PP('open_other_threads'), 'Can open threads started by others'),
+            (PP('delete_own_threads'), 'Can delete own threads'),
+            (PP('delete_other_threads'), 'Can delete threads started by others'),
+            (PP('move_own_threads'), 'Can move own threads'),
+            (PP('move_other_threads'), 'Can move threads started by others'),
+            (PP('start_threads'), 'Can start threads'),
+            (PP('reply_to_own_threads'), 'Can reply to own threads'),
+            (PP('reply_to_other_threads'), 'Can reply to threads started by others'),
+            (PP('edit_own_replies'), 'Can edit own replies'),
+            (PP('edit_other_replies'), 'Can edit replies posted by others'),
+            (PP('delete_own_replies'), 'Can delete own replies'),
+            (PP('delete_other_replies'), 'Can delete replies posted by others'),
+            (PP('attach_files'), 'Can attach files to posts'),
+            (PP('download_attachments'), 'Can download attachments'),
+            ('search_posts', 'Can search'),
+            ('view_profiles', 'Can view user profiles'),
+            ('view_forum_home', 'Can view forum home'),
         )
 
+class UserGroupManager(models.Manager):
+    def active(self):
+        site = Site.objects.get_current()
+        return self.get_query_set().filter(is_active=True, site=site)
+
+    def default(self):
+        group, created = self.active().get_or_create(is_default=True)
+        if created:
+            group.name = 'Member'
+            group.save()
+        return group
+
+class UserGroup(Group):
+    site = models.ForeignKey(Site, default=Site.objects.get_current)
+    is_active = models.BooleanField(blank=True)
+    is_default = models.BooleanField(blank=True)
+
+    objects = UserGroupManager()
+
 class ForumManager(models.Manager):
     _path_cache = {}
 
         return self._hierarchy
     hierarchy = property(_get_hierarchy)
 
+    def _get_last_post_info(self):
+        if self.last_post:
+            params = (
+                self.last_post.date_created.strftime('%d %b %y at %H:%M:%S'),
+                timesince(self.last_post.date_created),
+            )
+            return mark_safe('<abbr title="Posted %s">%s</abbr>' % params)
+    last_post_info = property(_get_last_post_info)
+
     def save(self, *args, **kwargs):
         """
         Ensures that this forum always has a unique slug and that all top-level
         return ('vcboard-show-post', [self.forum.path, self.id])
     get_absolute_url = models.permalink(get_absolute_url)
 
+    def _get_post_date_info(self):
+        params = (
+            self.date_created.strftime('%d %b %y at %H:%M:%S'),
+            timesince(self.date_created),
+        )
+        return mark_safe('<abbr title="Posted %s">%s</abbr>' % params)
+    post_date_info = property(_get_post_date_info)
+
     def _get_rate_count(self):
         """
         Determines how many times this post was rated
     def _get_author_link(self):
         if self.author:
             params = (
-                reverse('vcboard-user-profile', args=[self.id]),
+                reverse('vcboard-user-profile', args=[self.author.username]),
                 self.author.username
             )
             link = '<a href="%s" class="author-link">%s</a>' % params
 
     class Meta:
         ordering = ('date_created',)
-        permissions = (
-            ('show_post', _('Can view individual posts')),
-        )
 
 class Thread(Post):
     forum = models.ForeignKey(Forum, related_name='threads')
     view_count = models.PositiveIntegerField(_('Views'), default=0)
     is_sticky = models.BooleanField(_('Is Sticky'), blank=True, default=True, help_text=_('Sticky threads are always at the top of the threads in a forum.'))
     is_closed = models.BooleanField(_('Is Closed'), blank=True, default=False, help_text=_('Threads cannot be replied to once closed.'))
-    last_post = models.ForeignKey(Post, null=True, related_name='last_thread_post')
+    _last_post = models.ForeignKey(Post, null=True, related_name='last_thread_post')
 
     def __unicode__(self):
         return self.subject
 
+    def _get_last_post(self):
+        return self._last_post or self
+    def _set_last_post(self, post):
+        self._last_post = post
+    last_post = property(_get_last_post, _set_last_post)
+
+    def _get_last_post_info(self):
+        params = (
+            self.last_post.date_created.strftime('%d %b %y at %H:%M:%S'),
+            timesince(self.last_post.date_created),
+        )
+        return mark_safe('<abbr title="Posted %s">%s</abbr>' % params)
+    last_post_info = property(_get_last_post_info)
+
     def get_absolute_url(self):
         return ('vcboard-show-thread', [self.forum.path, self.id])
     get_absolute_url = models.permalink(get_absolute_url)
 
     class Meta:
         ordering = ('-date_created',)
-        permissions = (
-            ('can_sticky', _('Can sticky/unsticky threads')),
-            ('can_close', _('Can close/reopen threads')),
-            ('reply_to_thread', _('Can reply to threads')),
-        )
 
 class Rating(models.Model):
     post = models.ForeignKey(Post, related_name='ratings')
 class ThreadWatch(Watch):
     thread = models.ForeignKey(Thread, related_name='watching_users')
 
+class PermissionMatrix(models.Model):
+    site = models.ForeignKey(Site, default=Site.objects.get_current)
+    forum = models.ForeignKey(Forum)
+    permission = models.ForeignKey(Permission)
+    has_permission = models.NullBooleanField(default=None)
+
+    class Meta:
+        abstract = True
+
+class ForumPermission(PermissionMatrix):
+    pass
+
+class GroupPermission(PermissionMatrix):
+    group = models.ForeignKey(UserGroup)
+
+class UserPermission(PermissionMatrix):
+    user = models.ForeignKey(User)
+
 class ForumProfile(models.Model):
     user = models.OneToOneField(User)
+    group = models.ForeignKey(UserGroup, null=True, related_name='members')
     thread_count = models.PositiveIntegerField(default=0)
     post_count = models.PositiveIntegerField(default=0)
     date_created = models.DateTimeField(auto_now_add=True)
     date_updated = models.DateTimeField(auto_now=True)
 
-    def __getattr__(self, key):
-        """
-        Attempts to find a dynamic attribute for this user based on the key
-        """
-        if not key.startswith('_'):
-            # look for attributes for this user's profile
-            pass
+def get_profile(user):
+    if not hasattr(user, '_profile'):
+        user._profile = ForumProfile.objects.get_or_create(user=user)[0]
+        if not user._profile.group:
+            user._profile.group = UserGroup.objects.default()
+            user._profile.save()
+    return user._profile
+User.forumprofile = property(get_profile)
+AnonymousUser.forumprofile = ForumProfile()
 
-def get_profile(user):
-    # This will retrieve and cache a user's profile for the forum.  If a user
-    # does not have a profile, one will be created for them.
-    if not hasattr(user, '_forum_profile'):
-        user._forum_profile = ForumProfile.objects.get_or_create(user=user)[0]
-    return user._forum_profile
-User.forum_profile = property(get_profile)
-AnonymousUser.forum_profile = ForumProfile()

vcboard/ranks/models.py

 from django.db import models
 from django.utils.translation import ugettext_lazy as _
-from django.contrib.auth.models import User, Permission
-from vcboard.models import Forum, ForumProfile
+from django.contrib.auth.models import User
+from vcboard.models import Forum, ForumProfile, PermissionMatrix
 
 class RankManager(models.Manager):
     def active(self):
 
     objects = RankManager()
 
-    def has_perm(self, forum, permission):
-        """
-        Determines whether or not this Rank has a particular permission for
-        the specified forum
-        """
-        qs = self.forum_permissions.filter(forum=forum,
-                                           permission__codename=permission)
-        if qs.count():
-            return True
-        return False
-
     class Meta:
         ordering = ('ordering', 'title')
 
-class RankPermissionManager(models.Manager):
-    def for_forum(self, forum):
-        return self.get_query_set().filter(forum=forum)
+class RankPermission(PermissionMatrix):
+    rank = models.ForeignKey(Rank)
 
-class RankPermission(models.Model):
-    rank = models.ForeignKey(Rank, related_name='forum_permissions')
-    forum = models.ForeignKey(Forum, null=True, blank=True)
-    permissions = models.ManyToManyField(Permission, symmetrical=False)
-
-    objects = RankPermissionManager()
-
-    class Meta:
-        verbose_name = _('Permission')
-        verbose_name_plural = _('Permissions')
-
-def get_rank(user):
+def get_rank(forumprofile):
     """
     Determines a user's rank
     """
-    if not hasattr(user.forum_profile, '_rank'):
-        if user.ranks.count():
-            rank = user.ranks.all()[0]
+    if not hasattr(forumprofile, '_rank'):
+        if forumprofile.user.ranks.count():
+            rank = forumprofile.user.ranks.all()[0]
         else:
             try:
-                qs = Rank.objects.active()
-                qs = qs.filter(posts_required__lte=user.post_count)
+                qs = Rank.objects.active().filter(is_special=False)
+                qs = qs.filter(posts_required__lte=forumprofile.post_count)
                 rank = qs.order_by('-posts_required')[0]
             except IndexError:
                 rank = Rank()
-        user.forum_profile._rank = rank
-    return user.forum_profile._rank
+        forumprofile._rank = rank
+    return forumprofile._rank
 ForumProfile.rank = property(get_rank)
 

vcboard/templates/admin/permission_matrix.html

+{% extends 'admin/base_site.html' %}
+{% load i18n vcboard_tags %}
+
+{% block title %}{% trans "Permission Matrix" %}{{ block.super }}{% endblock %}
+
+{% block extrahead %}
+<script type="text/javascript" src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js"></script>
+<script type="text/javascript">
+var forum_ids = [{% for f in form.forums %}{{ f.id }}{% if not forloop.last %}, {% endif %}{% endfor %}];
+var perm_ids = [{% for p in form.permissions %}{{ p.id }}{% if not forloop.last %}, {% endif %}{% endfor %}];
+$(document).ready(function () {
+    // allow the user to select all permissions in a particular forum
+    $('input.select-all-none-forum').live('click', function(e) {
+        var checked = $('#' + e.target.id).attr('checked')
+        var value = checked ? 'on' : '';
+        for (var i = 0; i < perm_ids.length; i++) {
+            var pid = perm_ids[i];
+            var fid = '#id_' + e.target.id + pid;
+            $(fid).attr('checked', value)
+        }
+    });
+
+    // allow the user to select a permission for all forums
+    $('input.select-all-none-perm').live('click', function(e) {
+        var checked = $('#' + e.target.id).attr('checked')
+        var value = checked ? 'on' : '';
+        for (var i = 0; i < forum_ids.length; i++) {
+            var fid = forum_ids[i];
+            var pid = '#id_f_' + fid + e.target.id;
+            $(pid).attr('checked', value)
+        }
+    });
+
+    // allow the user to select everything or nothing quickly
+    $('input#select-everything').live('click', function(e) {
+        var checked = $('#select-everything').attr('checked')
+        var value = checked ? 'on' : '';
+        for (var f = 0; f < forum_ids.length; f++) {
+            var fid = forum_ids[f];
+            $('#f_' + fid + '_p_').attr('checked', value);
+            for (var p = 0; p < perm_ids.length; p++) {
+                var pid = perm_ids[p];
+                $('#id_f_' + fid + '_p_' + pid).attr('checked', value);
+            }
+        }
+
+        // Now select all of the "select all/none" checkboxes for permissions
+        for (var p = 0; p < perm_ids.length; p++) {
+            var pid = perm_ids[p];
+            $('#_p_' + pid).attr('checked', value);
+        }
+    });
+});
+</script>
+<style type="text/css">
+#permission-matrix td {
+    text-align: center;
+}
+</style>
+{% endblock %}
+
+{% block breadcrumbs %}
+    <div class="breadcrumbs">
+        <a href="../../">{% trans "Home" %}</a> &rsaquo; 
+        {% trans "Permission Matrix" %} &rsaquo;
+        {{ object }}
+    </div>
+{% endblock %}
+
+{% block content %}
+<h3>{% trans "Permissions For" %} {{ object }}</h3>
+<form action="." method="post">
+<table id="permission-matrix">
+    <tr class="header">
+        <th>Permission</th>
+        {% ifnotequal form.forums|length 1 %}
+        <th>
+            {% trans "Select<br />All/None" %}
+        </th>
+        {% endifnotequal %}
+        {% for forum in form.forums %}
+        <th>
+            {{ forum.name }}
+            {% if forum.parent %}<div class="quiet">{{ forum.parent }}</div>{% endif %}
+        </th>
+        {% endfor %}
+    </tr>
+    <tr class="select-all">
+        <th>{% trans "Select All/None" %}</th>
+        {% ifnotequal form.forums|length 1 %}
+        <td>
+            <input type="checkbox" class="select-all-none" id="select-everything" />
+        </td>
+        {% endifnotequal %}
+        {% for forum in form.forums %}
+        <td>
+            <input type="checkbox" class="select-all-none-forum" id="f_{{ forum.id }}_p_" />
+        </td>
+        {% endfor %}
+    </tr>
+    {% for perm in form.permissions %}
+    <tr class="{% cycle "odd" "even" %}">
+        <th>{{ perm.name }}</th>
+        {% ifnotequal form.forums|length 1 %}
+        <td>
+            <input type="checkbox" class="select-all-none-perm" id="_p_{{ perm.id }}" />
+        </td>
+        {% endifnotequal %}
+        {% for forum in form.forums %}
+        <td>
+            {% get_matrix_field form forum perm %}
+        </td>
+        {% endfor %}
+    </tr>
+    {% endfor %}
+</table>
+<p>{% trans "Saving permissions may take a bit of time. Please be patient." %}</p>
+<input type="submit" value="Save Permissions" />
+</form> 
+{% endblock %}

vcboard/templates/vcboard/_category_detail.html

         </td>
         <td class="forum-threads">{{ subforum.thread_count }}</td>
         <td class="forum-posts">{{ subforum.post_count }}</td>
-        <td class="forum-last-post">{{ subforum.last_post }}</td>
+        <td class="forum-last-post">
+            {% if subforum.last_post %}
+            <a href="{{ subforum.last_post.get_absolute_url }}">{{ subforum.last_post }}</a>
+            {% trans "by" %} {{ subforum.last_post.author_link }}
+            {% trans "about" %} {{ subforum.last_post_info }} {% trans "ago" %}
+            {% else %}
+            {% trans "None" %}
+            {% endif %}
+        </td>
     </tr>
     {% endfor %}
 {% if forloop.last %}</table>{% endif %}

vcboard/templates/vcboard/_forum_controls.html

 {% load i18n vcboard_tags %}
 <div class="forum-controls">
 {% get_forum_perms forum as forum_perms %}
-{% if forum_perms.vcboard__add_thread and not forum.is_category %}
-    <a href="{% url vcboard-create-thread forum.path %}">{% trans 'Create Thread' %}</a>
+{% if not forum.is_category %}
+{% if forum_perms.start_threads %}
+    <a href="{% url vcboard-create-thread forum.path %}">{% trans 'Start Thread' %}</a>
+{% endif %}
 {% endif %}
 </div>

vcboard/templates/vcboard/_forum_threads.html

         <td class="thread-replies">{{ thread.reply_count }}</td>
         <td class="thread-views">{{ thread.view_count }}</td>
         <td class="thread-last-post">
-            {{ thread.last_post.date_created|timesince }}
-            {% trans "by" %} {{ thread.last_post.author_link }}
+            {{ thread.last_post_info }} {% trans "ago by" %}
+            {{ thread.last_post.author_link }}
         </td>
     </tr>
     {% empty %}

vcboard/templates/vcboard/_thread_controls.html

+{% load i18n vcboard_tags %}
+<div class="thread-controls">
+{% ifequal thread.author user %}
+{% if forum_perms.reply_to_own_threads %}
+    <a href="{% url vcboard-create-reply forum.path thread.id %}" class="btn-thread-reply">{% trans 'Reply' %}</a>
+{% endif %}
+{% if forum_perms.edit_own_threads %}
+    <a href="{% url vcboard-edit-thread forum.path thread.id %}" class="btn-thread-edit">{% trans 'Edit Thread' %}</a>
+{% endif %}
+{% if forum_perms.delete_own_threads %}
+    <a href="{% url vcboard-delete-thread forum.path thread.id %}" class="btn-thread-delete">{% trans 'Delete Thread' %}</a>
+{% endif %}
+{% if forum_perms.close_own_threads and not thread.is_closed %}
+    <a href="{% url vcboard-close-thread forum.path thread.id %}" class="btn-thread-close">{% trans 'Close Thread' %}</a>
+{% endif %}
+{% if forum_perms.open_own_threads and thread.is_closed %}
+    <a href="{% url vcboard-open-thread forum.path thread.id %}" class="btn-thread-open">{% trans 'Re-open Thread' %}</a>
+{% endif %}
+{% if forum_perms.move_own_threads %}
+    <a href="{% url vcboard-move-thread forum.path thread.id %}" class="btn-thread-move">{% trans 'Move Thread' %}</a>
+{% endif %}
+{% else %}
+{% if forum_perms.reply_to_other_threads %}
+    <a href="{% url vcboard-create-reply forum.path thread.id %}" class="btn-thread-reply">{% trans 'Reply' %}</a>
+{% endif %}
+{% if forum_perms.edit_other_threads %}
+    <a href="{% url vcboard-edit-thread forum.path thread.id %}" class="btn-thread-edit">{% trans 'Edit Thread' %}</a>
+{% endif %}
+{% if forum_perms.delete_other_threads %}
+    <a href="{% url vcboard-delete-thread forum.path thread.id %}" class="btn-thread-edit">{% trans 'Delete Thread' %}</a>
+{% endif %}
+{% if forum_perms.close_other_thread and not thread.is_closed %}
+    <a href="{% url vcboard-close-thread forum.path thread.id %}" class="btn-thread-close">{% trans 'Close Thread' %}</a>
+{% endif %}
+{% if forum_perms.open_other_thread and thread.is_closed %}
+    <a href="{% url vcboard-open-thread forum.path thread.id %}" class="btn-thread-open">{% trans 'Re-open Thread' %}</a>
+{% endif %}
+{% if forum_perms.move_other_threads %}
+    <a href="{% url vcboard-move-thread forum.path thread.id %}" class="btn-thread-move">{% trans 'Move Thread' %}</a>
+{% endif %}
+{% endifequal %}
+</div>

vcboard/templates/vcboard/_thread_pagination.html

+{% load i18n %}
+<div class="pagination">
+    {% trans 'Pages:' %}
+    {% for p in paginator.page_range %}
+    {% ifequal p page.number %}
+        <span class="current-page">{{ p }}</span>
+    {% else %}
+        <a href="{% url vcboard-show-forum-page forum.path p %}">{{ p }}</a>
+    {% endifequal %}
+    {% endfor %}
+</div>

vcboard/templates/vcboard/_thread_post.html

-{% load i18n %}
+{% load i18n humanize %}
 <tr class="thread-{% cycle "odd" "even" %}">
-    <td class="author-info">
+    <td class="author-info" rowspan="2">
         {{ post.author_link }}
         {% if post.author %}
         <div class="stats">
             <div class="registered">
                 {% trans "Member since:" %}
-                {{ post.author.join_date }}
+                {{ post.author.date_joined|naturalday }}
             </div>
             <div class="posted">
                 {% trans "Posted:" %}
-                {{ post.date_created|timesince }}
+                {{ post.post_date_info }}
                 {% trans "ago" %}
             </div>
         </div>
         {% endif %}
     </td>
     <td class="comments">
+        <h6 class="post-subject">{% trans "Subject:" %} {{ post.subject }}</h6>
         {{ post.content }}
     </td>
 </tr>
+<tr>
+    <td class="post-controls">
+        {% ifequal post.author user %}
+        {% if forum_perms.reply_to_own_threads %}
+        <a href="{% url vcboard-create-reply forum.path thread.id %}?quoting={{ post.id }}" class="btn-post-quote">{% trans 'Quote' %}</a>
+        {% endif %}
+        {% if forum_perms.edit_own_replies and post.parent %}
+        <a href="{% url vcboard-edit-post forum.path post.id %}" class="btn-post-edit">{% trans 'Edit' %}</a>
+        {% endif %}
+        {% if forum_perms.delete_own_replies and post.parent %}
+        <a href="{% url vcboard-delete-post forum.path post.id %}" class="btn-post-delete">{% trans 'Delete' %}</a>
+        {% endif %}
+        {% else %}
+        {% if forum_perms.reply_to_other_threads %}
+        <a href="{% url vcboard-create-reply forum.path thread.id %}?quoting={{ post.id }}" class="btn-post-quote">{% trans 'Quote' %}</a>
+        {% endif %}
+        {% if forum_perms.edit_other_replies and post.parent %}
+        <a href="{% url vcboard-edit-post forum.path post.id %}" class="btn-post-edit">{% trans 'Edit' %}</a>
+        {% endif %}
+        {% if forum_perms.delete_other_replies and post.parent %}
+        <a href="{% url vcboard-delete-post forum.path post.id %}" class="btn-post-delete">{% trans 'Delete' %}</a>
+        {% endif %}
+        {% endifequal %}
+    </td>
+</tr>

vcboard/templates/vcboard/move_thread.html

+{% extends 'vcboard/base.html' %}
+{% load i18n vcboard_tags %}
+
+{% block title %}{{ block.super }}: {% trans "Move" %} {{ thread }}{% endblock %}
+{% block vc-breadcrumb %}
+{{ block.super }} {% for f in forum.hierarchy %}
+&rsaquo; <a href="{{ f.get_absolute_url }}">{{ f.name }}</a>
+{% endfor %} &rsaquo; <a href="{{ thread.get_absolute_url }}">{{ thread.subject }}</a>
+&rsaquo; {% trans "Move Thread" %}
+{% endblock %}
+
+{% block vc-content %}
+<h2>{% trans "Move Thread:" %} {{ thread.subject }}</h2>
+
+<form action="." method="post">
+<p>{% blocktrans %}Please choose the forum to which you would like to move this thread.{% endblocktrans %}</p>
+
+{% if error %}<div class="error">{{ error }}</div>{% endif %}
+
+<select name="move_to_forum">
+{% for f in valid_forums %}<option value="{{ f.id }}">{{ f }}</option>
+{% endfor %}
+</select><br />
+<input type="submit" value="{% trans "Move Thread" %}" />
+</form>
+{% endblock %}

vcboard/templates/vcboard/post_form.html

+{% extends 'vcboard/base.html' %}
+{% load i18n %}
+
+{% block title %}{{ block.super }}: {% trans 'Post Reply' %}{% endblock %}
+
+{% block vc-breadcrumb %}
+{{ block.super }} {% for f in forum.hierarchy %}
+&rsaquo; <a href="{{ f.get_absolute_url }}">{{ f.name }}</a>
+{% endfor %} &rsaquo; <a href="{{ thread.get_absolute_url }}">{{ thread.subject }}</a>
+&rsaquo; {% trans 'Post Reply' %}
+{% endblock %}
+
+{% block vc-content %}
+<h2>{% trans 'Post Reply' %}</h2>
+<form action="." method="post">
+<table class="post-form">
+    {{ form }}
+    <tr>
+        <td class="buttons" colspan="2">
+            <label for="save-post">&nbsp;</label>
+            <input type="submit" id="save-post" value="{% trans 'Post Reply' %}" />
+        </td>
+    </tr>
+</table>
+</form>
+{% endblock %}

vcboard/templates/vcboard/thread_detail.html

 {% extends 'vcboard/base.html' %}
-{% load i18n %}
+{% load i18n vcboard_tags %}
 
 {% block title %}{{ block.super }}: {{ thread }}{% endblock %}
 {% block vc-breadcrumb %}
 {% endblock %}
 
 {% block vc-content %}
+<h2>{{ thread.subject }}</h2>
+{% get_forum_perms forum as forum_perms %}
+
+{% include 'vcboard/_thread_controls.html' %}
 <table class="thread-table">
     {% with thread as post %}
     {% include 'vcboard/_thread_post.html' %}
     {% include 'vcboard/_thread_post.html' %}
     {% endfor %}
 </table>
+{% include 'vcboard/_thread_controls.html' %}
+{% include 'vcboard/_thread_pagination.html' %}
 {% endblock %}

vcboard/templatetags/vcboard_tags.py

 from django import template
 from django.contrib.auth.models import Permission
 from vcboard.models import Forum, Thread
-from vcboard.utils import PermissionBot
+from vcboard.utils import get_user_permissions
 from datetime import datetime
 try:
     set
 
 register = template.Library()
 
+@register.simple_tag
+def get_matrix_field(form, forum, permission):
+    """
+    Makes it possible to render the permission matrix form fields individually
+    """
+    key = 'f_%i_p_%i' % (forum.id, permission.id)
+    attr = {'id': 'id_' + key}
+    if form.fields.has_key(key) and form.fields[key].initial:
+        attr['checked'] = 'on'
+    return form.fields[key].widget.render(key, form.data.get(key, None), attr)
+
 class HasUnreadInNode(template.Node):
     def __init__(self, obj, variable):
         self.obj = template.Variable(obj)
     tag, obj, a, variable = bits
     return HasUnreadInNode(obj, variable)
 
-class GetForumPerms(template.Node):
+class GetPermsNode(template.Node):
     def __init__(self, forum, variable):
         self.forum = template.Variable(forum)
         self.variable = variable
         forum = self.forum.resolve(context)
         user = context.get('user', None)
         if user:
-            context[self.variable] = PermissionBot(user, forum)
+            context[self.variable] = get_user_permissions(user, forum)
         return ''
 
 @register.tag
     if len(bits) != 4:
         raise template.TemplateSyntaxError('get_forum_perms syntax: {% get_forum_perms forum as perms %}')
     tag, forum, a, variable = bits
-    return GetForumPerms(forum, variable)
+    return GetPermsNode(forum, variable)
 
 # TODO: implement URLs for other VCBoard apps here, before the catch-all ?P<path>
 
-urlpatterns = patterns('',
+urlpatterns = patterns('vcboard.admin_views',
+    url(r'^permissions/(?P<obj_type>\w+)/(?P<id>\d+)/$', 'permission_matrix', name='vcboard-permissions'),
+)
+
+pre = lambda p: r'^forum/(?P<path>.*)/%s' % p
+
+urlpatterns += patterns('',
     url(r'^$', views.forum_home, name='vcboard-home'),
-    url(r'^(?P<path>.*)/thread/(?P<thread_id>\d+)/edit/$', 
+    url(r'^profile/(?P<username>[\w\-]+)/$',
+        views.user_profile,
+        name='vcboard-user-profile'),
+    url(pre(r'thread/(?P<thread_id>\d+)/edit/$'), 
         views.edit_thread, 
         name='vcboard-edit-thread'),
-    url(r'^(?P<path>.*)/thread/(?P<thread_id>\d+)/reply/$', 
+    url(pre(r'thread/(?P<thread_id>\d+)/delete/$'), 
+        views.edit_thread, 
+        name='vcboard-delete-thread'),
+    url(pre(r'thread/(?P<thread_id>\d+)/move/$'), 
+        views.move_thread, 
+        name='vcboard-move-thread'),
+    url(pre('thread/(?P<thread_id>\d+)/reply/$'), 
         views.post_reply, 
         name='vcboard-create-reply'),
-    url(r'^(?P<path>.*)/thread/(?P<thread_id>\d+)/page/(?P<page>\d+)/$', 
+    url(pre('thread/(?P<thread_id>\d+)/close/$'), 
+        views.close_thread, 
+        name='vcboard-close-thread'),
+    url(pre('thread/(?P<thread_id>\d+)/open/$'), 
+        views.open_thread, 
+        name='vcboard-open-thread'),
+    url(pre('thread/(?P<thread_id>\d+)/page/(?P<page>\d+)/$'), 
         views.show_thread, 
         name='vcboard-show-thread-page'),
-    url(r'^(?P<path>.*)/thread/(?P<thread_id>\d+)/$', 
+    url(pre('thread/(?P<thread_id>\d+)/$'), 
         views.show_thread, 
         name='vcboard-show-thread'),
-    url(r'^(?P<path>.*)/post/(?P<post_id>\d+)/edit/$', 
+    url(pre('post/(?P<post_id>\d+)/edit/$'), 
         views.edit_post, 
         name='vcboard-edit-post'),
-    url(r'^(?P<path>.*)/post/(?P<post_id>\d+)/$', 
+    url(pre('post/(?P<post_id>\d+)/delete/$'), 
+        views.delete_post, 
+        name='vcboard-delete-post'),
+    url(pre('post/(?P<post_id>\d+)/$'), 
         views.show_post, 
         name='vcboard-show-post'),
-    url(r'^(?P<path>.*)/new/$', 
+    url(pre('new/$'), 
         views.create_thread, 
         name='vcboard-create-thread'),
-    url(r'^(?P<path>.*)/page/(?P<page>\d+)/$', 
+    url(pre('page/(?P<page>\d+)/$'), 
         views.show_forum, 
         name='vcboard-show-forum-page'),
     url(r'^(?P<path>.*)$', 
+from django.conf import settings
+from django.contrib.auth.models import AnonymousUser, Permission
+from django.core.cache import cache
 from django.shortcuts import render_to_response
 from django.template import RequestContext
 from django.template.defaultfilters import slugify
 
-PERM_MAP = {
-    'vcboard.view_forum_home': (
-        'general', 'anonymous_home', bool, True),
-    'vcboard.view_forum': (
-        'general', 'anonymous_forum', bool, True),
-    'vcboard.show_thread': (
-        'forum', 'anonymous_show_thread', bool, True),
-    'vcboard.add_thread': (
-        'forum', 'anonymous_add_thread', bool, False),
-    'vcboard.add_post': (
-        'thread', 'anonymous_add_post', bool, False),
-}
+# get the cache timeout from the settings, or default to 1 hour
+TIMEOUT = getattr(settings, 'CACHE_TIMEOUT', 3600)
 
-def can(user, permission, section=None, key=None, \
-        primitive=None, default=None, forum=None):
+PREFIX = 'vcb_'
+# prefix permissions so they're easier to differentiate
+PP = lambda s: '%s%s' % (PREFIX, s)
+
+# de-prefix a permission
+DP = lambda s: s.replace(PREFIX, '')
+
+def get_user_permissions(user, forum):
     """
-    Determines whether or not the specified user has the specified permission
+    Fetches a user's permissions for a particular forum
     """
-    from vcboard import config
-    if user.has_perm(permission) or \
-       (hasattr(user.forum_profile, 'rank') and \
-       user.forum_profile.rank != None and \
-       user.forum_profile.rank.has_perm(forum, permission)):
-        return True
 
-    if not (section or key):
-        print 'Looking for', permission
-        args = PERM_MAP.get(permission, ())
+    # try to pull the permissions from the cache
+    uid = user and user.id or 'anon'
+    cache_key = 'u%s_f%i' % (uid, forum.id)
+    cached_perm_dict = cache.get('vcboard_user_perms', {})
+    if cached_perm_dict.has_key(cache_key):
+        return cached_perm_dict[cache_key]
     else:
-        args = (section, key, primitive, default)
+        cached_perm_dict[cache_key] = {}
+    forum_perms = cache.get('perms_for_forums', {})
 
-    if len(args):
-        print 'Args', args,
-        print config(*args)
-        return config(*args)
-    return False
+    from vcboard.models import ForumPermission
+    if isinstance(user, AnonymousUser):
+        permissions = ForumPermission.objects.filter(forum__id=forum.id)
+        perm_dict = dict((DP(p.permission.codename), p.has_permission or False) for p in permissions)
+    else:
+        from django.db import connection
+        cur = connection.cursor()
+        qn = connection.ops.quote_name
+        permissions = Permission.objects.filter(codename__startswith='vcb_')
 
-class PermissionBot(object):
-    def __init__(self, user, forum):
-        self.user = user
-        self.forum = forum
+        # determine whether or not the ranks extension is installed
+        rank = rank_join = ''
+        if hasattr(user.forumprofile, 'rank') and user.forumprofile.rank:
+            rank_table = qn('ranks_rankpermission')
+            rank = 'rp.%s,' % qn('has_permission')
+            rank_join = 'LEFT OUTER JOIN %s rp ON rp.%s = f.%s AND rp.%s = %s AND rp.%s = %%(perm_id)s'
+            rank_join %= (rank_table, qn('forum_id'), qn('id'), 
+                          qn('rank_id'), user.forumprofile.rank.id or 0,
+                          qn('permission_id'))
 
-    def __getattr__(self, key):
-        key = key.replace('__', '.')
-        return can(self.user, key, forum=self.forum)
+        perm_dict = {}
+        query = '''
+        SELECT DISTINCT
+        COALESCE(up.%(hp)s, %(rank)s
+            gp.%(hp)s,
+            fp.%(hp)s) as %%(perm_name)s
+        FROM %(forums)s f
+        LEFT OUTER JOIN %(forum)s fp ON fp.%(fid)s = f.%(id)s AND fp.%(pid)s = %%(perm_id)s
+        LEFT OUTER JOIN %(group)s gp ON gp.%(fid)s = f.%(id)s AND gp.%(gid)s = %(group_id)s AND gp.%(pid)s = %%(perm_id)s
+        %(rank_join)s
+        LEFT OUTER JOIN %(user)s up ON up.%(fid)s = f.%(id)s AND up.%(uid)s = %(user_id)s AND up.%(pid)s = %%(perm_id)s
+        WHERE f.%(id)s = %(forum_id)s
+        ''' % {
+            'hp': qn('has_permission'),
+            'rank': rank,
+            'forums': qn('vcboard_forum'),
+            'forum': qn('vcboard_forumpermission'),
+            'group': qn('vcboard_grouppermission'),
+            'rank_join': rank_join,
+            'user': qn('vcboard_userpermission'),
+            'fid': qn('forum_id'),
+            'id': qn('id'),
+            'forum_id': forum.id,
+            'gid': qn('group_id'),
+            'pid': qn('permission_id'),
+            'group_id': user.forumprofile.group.id,
+            'uid': qn('user_id'),
+            'user_id': user.id,
+        }
+        for perm in permissions:
+            p = DP(perm.codename)
+            current = query % {'perm_name': qn(p), 'perm_id': perm.id}
+            #print 'hitting db:', current
+            cur.execute(current)
+            row = cur.fetchone()
+            if row:
+                perm_dict[p] = bool(row[0])
+            else:
+                perm_dict[p] = False
+
+    # cache the permissions
+    if not forum_perms.has_key(forum.id):
+        forum_perms[str(forum.id)] = []
+    forum_perms[str(forum.id)].append(cache_key)
+    cache.set('perms_for_forums', forum_perms)
+
+    cached_perm_dict[cache_key] = perm_dict
+    cache.set('vcboard_user_perms', cached_perm_dict, TIMEOUT)
+    return perm_dict
 
 def render(request, template, data, args=(), kwargs={}):
     """
+from django.conf import settings
 from django.contrib.auth.decorators import permission_required
+from django.contrib.auth.models import User
 from django.core.paginator import Paginator
 from django.http import Http404, HttpResponseRedirect
 from django.shortcuts import get_object_or_404
 from vcboard import config, decorators as vcb, signals
-from vcboard.forms import ThreadForm
-from vcboard.models import Forum, Thread
-from vcboard.utils import render
+from vcboard.forms import ThreadForm, ReplyForm
+from vcboard.models import Forum, Thread, Post
+from vcboard.utils import render, get_user_permissions
 
-@vcb.permission_required('vcboard.view_forum_home')
 def forum_home(request, template='vcboard/forum_home.html'):
     """
     Displays all of the top-level forums and other random forum info
     data = {'forums': Forum.objects.top_level()}
     return render(request, template, data)
 
-@vcb.permission_required('vcboard.view_forum', forum=True)
+@vcb.permission_required('view_forum')
 def show_forum(request, path, page=1, template='vcboard/forum_detail.html'):
     """
     Displays a forum with its subforums and topics, if any
 
     return render(request, template, data)
 
-@vcb.permission_required('vcboard.add_thread', forum=True)
+@vcb.permission_required('start_threads')
 def create_thread(request, path, template='vcboard/thread_form.html'):
     """
     Allows users to create a thread
 
     return render(request, template, data)
 
-@vcb.permission_required('vcboard.show_thread', forum=True)
-def show_thread(request, path, thread_id, page=1, 
-        template='vcboard/thread_detail.html'):
+def base_show_thread(request, forum, thread, page, template):
     """
-    Allows users to view threads
+    Does the work that allows users to view a thread
     """
-    forum = Forum.objects.with_path(path)
-    thread = get_object_or_404(Thread, pk=thread_id, forum=forum)
-    
     paginator = Paginator(thread.posts.valid(), config('thread', 
                                                'posts_per_page',
                                                int, 20))
     page_obj = paginator.page(page)
+    
+    # TODO: make the view count increment intelligently
+    thread.view_count += 1
+    thread.save()
 
     data = {
         'forum': forum,
 
     return render(request, template, data)
 
-@vcb.permission_required('vcboard.change_thread', forum=True)
-def edit_thread(request, path, thread_id, template='vcboard/thread_form.html'):
+@vcb.permission_required('view_other_threads')
+def show_other_thread(*args, **kwargs):
+    # wraps base_show_thread with a decorator that checks for permission
+    return base_show_thread(*args, **kwargs)
+
+def show_thread(request, path, thread_id, page=1, 
+        template='vcboard/thread_detail.html'):
     """
-    Allows users to edit threads that they created
+    Allows users to view threads
+    """
+    forum = Forum.objects.with_path(path)
+    thread = get_object_or_404(Thread, pk=thread_id, forum=forum)
+    func = base_show_thread
+    if thread.author != request.user:
+        func = show_other_thread
+    return func(request, forum, thread, page, template)
+
+def base_edit_thread(request, forum, thread, template):
+    """
+    Does the work that allows users to edit threads
     """
     data = {}
 
     return render(request, template, data)
 
-@vcb.permission_required('vcboard.reply_to_thread', forum=True)
+@vcb.permission_required('edit_other_threads')
+def edit_other_thread(*args, **kwargs):
+    return base_edit_thread(*args, **kwargs)
+
+def edit_thread(request, path, thread_id, template='vcboard/thread_form.html'):
+    """
+    Allows users to edit threads
+    """
+    forum = Forum.objects.with_path(path)
+    thread = get_object_or_404(Thread, pk=thread_id, forum=forum)
+    func = base_edit_thread
+    if thread.author != request.user:
+        func = edit_other_threads
+    return func(request, forum, thread, template)
+
+def base_post_reply(request, forum, thread, template):
+    """
+    Does the work that allows a user to reply to a thread
+    """
+    if request.method == 'POST':
+        form = ReplyForm(request.POST)
+        if form.is_valid():
+            post = form.save(commit=False)
+            post.parent = thread
+
+            if request.user.is_authenticated():
+                post.author = request.user
+            else:
+                post.is_draft = False
+
+            post.ip_address = request.META.get('REMOTE_ADDR', '127.0.0.1')
+            post.save()
+
+            return HttpResponseRedirect(thread.get_absolute_url())
+    else:
+        subject = thread.subject
+        if not subject.startswith('Re: '):
+            subject = 'Re: ' + subject
+
+        content = ''
+        quoting = int(request.GET.get('quoting', 0))
+        if quoting:
+            post = Post.objects.valid().get(pk=quoting)
+            content = '[quote id="%i"]%s[/quote]' % (post.id, post.content)
+
+        form = ReplyForm(initial={
+            'subject': subject,
+            'content': content
+        })
+
+    data = {
+        'form': form,
+        'forum': forum,
+        'thread': thread
+    }
+    return render(request, template, data)
+
+@vcb.permission_required('reply_to_other_threads')
+def reply_to_other(*args, **kwargs):
+    return base_post_reply(*args, **kwargs)
+
 def post_reply(request, path, thread_id, 
         template='vcboard/post_form.html'):
     """
     Allows the user to post a reply to a thread
     """
-    data = {}
-    return render(request, template, data)
+    forum = Forum.objects.with_path(path)
+    thread = get_object_or_404(Thread, pk=thread_id, forum=forum)
+    func = base_post_reply
+    if thread.author != request.user:
+        func = reply_to_other
+    return func(request, forum, thread, template)
 
-@vcb.permission_required('vcboard.show_post', forum=True)
+@vcb.permission_required('show_post')
 def show_post(request, path, post_id, template='vcboard/thread_detail.html'):
     """
     Allows users to view threads
 
     return render(request, template, data)
 
-@vcb.permission_required('vcboard.change_post', forum=True)
+@vcb.permission_required('change_post')
 def edit_post(request, path, post_id, 
         template='vcboard/post_form.html'):
     """
     """
     data = {}
     return render(request, template, data)
+
+@vcb.permission_required('view_profiles')
+def user_profile(request, username=None, template='vcboard/user_profile.html'):
+    """
+    Displays a user's profile
+    """
+    if not username:
+        user = request.user
+    else:
+        user = get_object_or_404(User, username=username)
+
+    data = {'user': user}
+
+    return render(request, template, data)
+
+def base_close_thread(request, forum, thread):
+    """
+    Does the work to close a thread
+    """
+    thread.is_closed = True
+    thread.save()
+    return HttpResponseRedirect(thread.get_absolute_url())
+
+@vcb.permission_required('close_other_threads')
+def close_other_threads(*args, **kwargs):
+    return base_close_thread(*args, **kwargs)
+
+def close_thread(request, path, thread_id):
+    forum = Forum.objects.with_path(path)
+    thread = get_object_or_404(Thread, pk=thread_id, forum=forum)
+    func = base_close_thread
+    if thread.author != request.user:
+        func = close_other_threads
+    return func(request, forum, thread)
+
+def base_open_thread(request, forum, thread):
+    """
+    Does the work to open a thread
+    """
+    thread.is_closed = False
+    thread.save()
+    return HttpResponseRedirect(thread.get_absolute_url())
+
+@vcb.permission_required('open_other_threads')
+def open_other_threads(*args, **kwargs):
+    return base_open_thread(*args, **kwargs)
+
+def open_thread(request, path, thread_id):
+    forum = Forum.objects.with_path(path)
+    thread = get_object_or_404(Thread, pk=thread_id, forum=forum)
+    func = base_open_thread
+    if thread.author != request.user:
+        func = open_other_threads
+    return func(request, forum, thread)
+
+def base_delete_thread(request, forum, thread):
+    """
+    Does the work to delete a thread
+    """
+    thread.is_deleted = False
+    thread.save()
+    return HttpResponseRedirect(forum.get_absolute_url())
+
+@vcb.permission_required('delete_other_threads')
+def delete_other_threads(*args, **kwargs):
+    return base_delete_thread(*args, **kwargs)
+
+def delete_thread(request, path, thread_id):
+    forum = Forum.objects.with_path(path)
+    thread = get_object_or_404(Thread, pk=thread_id, forum=forum)
+    func = base_delete_thread
+    if thread.author != request.user:
+        func = delete_other_threads
+    return func(request, forum, thread)
+
+def base_delete_post(request, forum, post):
+    """
+    Does the work to delete a post
+    """
+    post.is_deleted = False
+    post.save()
+    return HttpResponseRedirect(forum.get_absolute_url())
+
+@vcb.permission_required('delete_other_posts')
+def delete_other_posts(*args, **kwargs):
+    return base_delete_post(*args, **kwargs)
+
+def delete_post(request, path, post_id):
+    forum = Forum.objects.with_path(path)
+    post = get_object_or_404(Post, pk=post_id, parent__forum=forum)
+    func = base_delete_post
+    if post.author != request.user:
+        func = delete_other_posts
+    return func(request, forum, post)
+
+def base_move_thread(request, forum, thread, template):
+    """
+    Does the work to move a thread
+    """
+
+    # find all forums in which this user is allowed to start threads
+    postable = Forum.objects.active().exclude(pk=forum.id)
+    postable = postable.filter(is_category=False)
+    valid = []
+    error = None
+    for f in postable:
+        perms = get_user_permissions(request.user, f)
+        if perms['start_threads']:
+            valid.append(f)
+
+    if request.method == 'POST':
+        forum_id = int(request.POST.get('move_to_forum', 0))
+        f = Forum.objects.get(pk=forum_id)
+        if f in postable:
+            # reduce counts
+            for p in thread.forum.hierarchy:
+                p.thread_count -= 1
+                p.post_count -= thread.posts.count() - 1
+                p.save()
+
+                # TODO: look into updating the last post if the moving thread
+                # was the last post in this forum
+
+            # move the thread
+            thread.forum = f
+            thread.save()
+
+            # update counts again
+            for n in thread.forum.hierarchy:
+                n.thread_count += 1
+                n.post_count += thread.posts.count() + 1
+
+                if not n.last_post or n.last_post.date_created < thread.date_created:
+                    n.last_post = thread
+
+                n.save()
+
+            return HttpResponseRedirect(thread.get_absolute_url())
+        else:
+            error = _('Invalid forum!')
+
+    data = {
+        'forum': forum,
+        'thread': thread,
+        'valid_forums': valid,
+        'error': error,
+    }
+
+    return render(request, template, data)
+
+@vcb.permission_required('move_other_threads')
+def move_other_threads(*args, **kwargs):
+    return base_move_thread(*args, **kwargs)
+
+def move_thread(request, path, thread_id, template='vcboard/move_thread.html'):
+    forum = Forum.objects.with_path(path)
+    thread = get_object_or_404(Thread, pk=thread_id, forum=forum)
+    func = base_move_thread
+    if thread.author != request.user:
+        func = move_other_threads
+    return func(request, forum, thread, template)
+
+if getattr(settings, 'VCBOARD_LOGIN_REQUIRED', False):
+    forum_home = permission_required('vcboard.view_forum_home')(forum_home)