Rafał Pitoń 11 лет назад
Родитель
Сommit
dcb343a9dd

+ 0 - 0
misago/apps/admin/prefixes/__init__.py


+ 21 - 0
misago/apps/admin/prefixes/forms.py

@@ -0,0 +1,21 @@
+from django.core.validators import RegexValidator
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.forms import Form, ForumMultipleChoiceField
+from misago.models import Role, Forum
+from misago.validators import validate_sluggable
+
+class PrefixForm(Form):
+    name = forms.CharField(label=_("Prefix Name"),
+                           max_length=16, validators=[validate_sluggable(
+                                                                          _("Prefix must contain alphanumeric characters."),
+                                                                          _("Prefix name is too long.")
+                                                                          )])
+    style = forms.CharField(label=_("Prefix CSS Class"),
+                            help_text=_("CSS class that will be used to style this thread prefix."),
+                            max_length=255, required=False)
+
+    forums = ForumMultipleChoiceField(label=_("Prefix Forums"),
+                                      help_text=_("Select forums in which this prefix will be available."),
+                                      level_indicator=u'- - ',
+                                      queryset=Forum.objects.get(special='root').get_descendants())

+ 121 - 0
misago/apps/admin/prefixes/views.py

@@ -0,0 +1,121 @@
+from django.core.urlresolvers import reverse as django_reverse
+from django.utils.translation import ugettext as _
+import floppyforms as forms
+from misago import messages
+from misago.admin import site
+from misago.apps.admin.widgets import *
+from misago.forms import Form
+from misago.models import ThreadPrefix
+from misago.utils.strings import slugify
+from misago.apps.admin.prefixes.forms import PrefixForm
+
+def reverse(route, target=None):
+    if target:
+        return django_reverse(route, kwargs={'target': target.pk, 'slug': slugify(target.name)})
+    return django_reverse(route)
+
+
+"""
+Views
+"""
+class List(ListWidget):
+    admin = site.get_action('prefixes')
+    id = 'list'
+    columns = (
+               ('prefix', _("Thread Prefix")),
+               )
+    nothing_checked_message = _('You have to check at least one prefix.')
+    actions = (
+               ('delete', _("Delete selected prefixes"), _("Are you sure you want to delete selected prefixes?")),
+               )
+
+    def table_action(self, page_items, cleaned_data):
+        for item in page_items:
+            item.order = cleaned_data['pos_' + str(item.pk)]
+            item.save(force_update=True)
+        return Message(_('Prefixes order has been changed'), messages.SUCCESS), reverse('admin_threads_prefixes')
+
+    def sort_items(self, page_items, sorting_method):
+        return page_items.order_by('name')
+
+    def get_item_actions(self, item):
+        return (
+                self.action('pencil', _("Edit Prefix"), reverse('admin_threads_prefixes_edit', item)),
+                self.action('remove', _("Delete Prefix"), reverse('admin_threads_prefixes_delete', item), post=True, prompt=_("Are you sure you want to delete this prefix?")),
+                )
+
+    def action_delete(self, items, checked):
+        for prefix in ThreadPrefix.objects.filter(id__in=checked):
+            prefix.delete()
+        return Message(_('Selected prefixes have been deleted successfully.'), messages.SUCCESS), reverse('admin_threads_prefixes')
+
+
+class New(FormWidget):
+    admin = site.get_action('prefixes')
+    id = 'new'
+    fallback = 'admin_threads_prefixes'
+    form = PrefixForm
+    submit_button = _("Save Prefix")
+
+    def get_new_link(self, model):
+        return reverse('admin_threads_prefixes_new')
+
+    def get_edit_link(self, model):
+        return reverse('admin_threads_prefixes_edit', model)
+
+    def submit_form(self, form, target):
+        new_prefix = ThreadPrefix(
+                                  name=form.cleaned_data['name'],
+                                  slug=slugify(form.cleaned_data['name']),
+                                  style=form.cleaned_data['style'],
+                                  )
+        new_prefix.save(force_insert=True)
+        for forum in form.cleaned_data['forums']:
+            new_prefix.forums.add(forum)
+        return new_prefix, Message(_('New Prefix has been created.'), messages.SUCCESS)
+
+
+class Edit(FormWidget):
+    admin = site.get_action('prefixes')
+    id = 'edit'
+    name = _("Edit Prefix")
+    fallback = 'admin_threads_prefixes'
+    form = PrefixForm
+    target_name = 'name'
+    notfound_message = _('Requested Prefix could not be found.')
+    translate_target_name = True
+    submit_fallback = True
+
+    def get_link(self, model):
+        return reverse('admin_threads_prefixes_edit', model)
+
+    def get_edit_link(self, model):
+        return self.get_link(model)
+
+    def get_initial_data(self, model):
+        return {
+                'name': model.name,
+                'style': model.style,
+                'forums': model.forums.all(),
+                }
+
+    def submit_form(self, form, target):
+        target.name = form.cleaned_data['name']
+        target.slug = slugify(form.cleaned_data['name'])
+        target.style = form.cleaned_data['style']
+        target.save(force_update=True)
+
+        target.update_forums(form.cleaned_data['forums'])
+
+        return target, Message(_('Changes in prefix "%(name)s" have been saved.') % {'name': self.original_name}, messages.SUCCESS)
+
+
+class Delete(ButtonWidget):
+    admin = site.get_action('prefixes')
+    id = 'delete'
+    fallback = 'admin_threads_prefixes'
+    notfound_message = _('Requested Prefix could not be found.')
+
+    def action(self, target):
+        target.delete()
+        return Message(_('Prefix "%(name)s" has been deleted.') % {'name': _(target.name)}, messages.SUCCESS), False

+ 23 - 5
misago/apps/admin/sections/forums.py

@@ -1,7 +1,7 @@
 from django.conf.urls import patterns, include, url
 from django.utils.translation import ugettext_lazy as _
 from misago.admin import AdminAction
-from misago.models import Forum
+from misago.models import Forum, ThreadPrefix
 
 ADMIN_ACTIONS = (
     AdminAction(
@@ -43,10 +43,28 @@ ADMIN_ACTIONS = (
                 name=_("Thread Prefixes"),
                 help=_("Thread Prefix allow you to group and classify threads together within forums."),
                 icon='tags',
-                link='admin_forums_prefixes',
-                urlpatterns=patterns('misago.apps.admin.index',
-                        url(r'^$', 'todo', name='admin_forums_prefixes'),
-                    ),
+                model=ThreadPrefix,
+                actions=[
+                         {
+                          'id': 'list',
+                          'name': _("Prefixes List"),
+                          'help': _("All existing prefixes"),
+                          'link': 'admin_threads_prefixes'
+                          },
+                         {
+                          'id': 'new',
+                          'name': _("Add Prefix"),
+                          'help': _("Create new threads prefix"),
+                          'link': 'admin_threads_prefixes_new'
+                          },
+                         ],
+                link='admin_threads_prefixes',
+                urlpatterns=patterns('misago.apps.admin.prefixes.views',
+                         url(r'^$', 'List', name='admin_threads_prefixes'),
+                         url(r'^new/$', 'New', name='admin_threads_prefixes_new'),
+                         url(r'^edit/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Edit', name='admin_threads_prefixes_edit'),
+                         url(r'^delete/(?P<slug>([a-z0-9]|-)+)-(?P<target>\d+)/$', 'Delete', name='admin_threads_prefixes_delete'),
+                     ),
                 ),
     AdminAction(
                 section='forums',

+ 61 - 2
misago/apps/threads/list.py

@@ -3,9 +3,13 @@ from django.core.urlresolvers import reverse
 from django.http import Http404
 from django.shortcuts import redirect
 from django.utils.translation import ugettext as _
+from misago import messages
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.errors import error403, error404
 from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
 from misago.conf import settings
-from misago.models import Forum, Thread
+from misago.decorators import check_csrf
+from misago.models import Forum, Thread, ThreadPrefix
 from misago.readstrackers import ThreadsTracker
 from misago.utils.pagination import make_pagination
 from misago.apps.threads.mixins import TypeMixin
@@ -14,10 +18,24 @@ class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
     def fetch_forum(self):
         self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
 
+        self.prefixes = ThreadPrefix.objects.forum_prefixes(self.forum)
+        self.active_prefix = self.request.session.get('forum_prefix_%s' % self.forum.pk)
+
+        if self.active_prefix and self.active_prefix.pk not in self.prefixes:
+            self.active_prefix = None
+
+    def template_vars(self, context):
+        context['prefixes'] = self.prefixes
+        context['active_prefix'] = self.active_prefix
+        return context
+
     def threads_queryset(self):
         announcements = self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set).filter(weight=2).order_by('-pk')
         threads = self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set).filter(weight__lt=2).order_by('-weight', '-last')
 
+        if self.active_prefix:
+            threads = threads.filter(prefix=self.active_prefix)
+
         # Dont display threads by ignored users (unless they are important)
         if self.request.user.is_authenticated():
             ignored_users = self.request.user.ignored_users()
@@ -70,4 +88,45 @@ class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
                 actions.append(('hard', _('Delete threads')))
         except KeyError:
             pass
-        return actions
+        return actions
+
+
+class ForumSwitchThreadPrefix(ThreadsListView):
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.kwargs = kwargs
+        self.pagination = {}
+        self.parents = []
+        self.threads = []
+        self.message = request.messages.get_message('threads')
+        try:
+            self._type_available()
+            self._fetch_forum()
+            return self.change_prefix()
+        except (Forum.DoesNotExist, Thread.DoesNotExist):
+            return error404(request)
+        except ACLError403 as e:
+            return error403(request, unicode(e))
+        except ACLError404 as e:
+            return error404(request, unicode(e))
+
+    def change_prefix(self):
+        @check_csrf
+        def view(request):
+            session_key = 'forum_prefix_%s' % self.forum.pk
+            try:
+                new_prefix = int(self.request.POST.get('switch_prefix', 0))
+            except ValueError:
+                new_prefix = 0
+
+            if self.prefixes and new_prefix in self.prefixes:
+                self.request.session[session_key] = self.prefixes[new_prefix]
+                messages.info(self.request, _('Displaying only threads that are prefixed with "%(prefix)s".') % {'prefix': _(self.prefixes[new_prefix].name)}, 'threads')
+            else:
+                self.request.session[session_key] = None
+                messages.info(self.request, _("Displaying all threads."), 'threads')
+
+            if 'retreat' in self.request.POST:
+                return redirect(self.request.POST.get('retreat'))
+            return self.threads_list_redirect()
+        return view(self.request)

+ 1 - 0
misago/apps/threads/urls.py

@@ -3,6 +3,7 @@ from django.conf.urls import patterns, url
 urlpatterns = patterns('misago.apps.threads',
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'list.ThreadsListView', name="forum"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>[1-9]([0-9]+)?)/$', 'list.ThreadsListView', name="forum"),
+    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/switch-prefix/$', 'list.ForumSwitchThreadPrefix', name="forum_switch_prefix"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/start/$', 'posting.NewThreadView', name="thread_start"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="thread_edit"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),

+ 463 - 0
misago/migrations/0026_auto__add_threadprefix.py

@@ -0,0 +1,463 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'ThreadPrefix'
+        db.create_table(u'misago_threadprefix', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=255)),
+            ('style', self.gf('django.db.models.fields.CharField')(max_length=255)),
+        ))
+        db.send_create_signal('misago', ['ThreadPrefix'])
+
+        # Adding M2M table for field forums on 'ThreadPrefix'
+        m2m_table_name = db.shorten_name(u'misago_threadprefix_forums')
+        db.create_table(m2m_table_name, (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('threadprefix', models.ForeignKey(orm['misago.threadprefix'], null=False)),
+            ('forum', models.ForeignKey(orm['misago.forum'], null=False))
+        ))
+        db.create_unique(m2m_table_name, ['threadprefix_id', 'forum_id'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'ThreadPrefix'
+        db.delete_table(u'misago_threadprefix')
+
+        # Removing M2M table for field forums on 'ThreadPrefix'
+        db.delete_table(db.shorten_name(u'misago_threadprefix_forums'))
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'int'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.poll': {
+            'Meta': {'object_name': 'Poll'},
+            '_choices_cache': ('django.db.models.fields.TextField', [], {'db_column': "'choices_cache'"}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            'length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'max_choices': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'question': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'poll_of'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'vote_changing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.polloption': {
+            'Meta': {'object_name': 'PollOption'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'poll': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'option_set'", 'to': "orm['misago.Poll']"}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.pollvote': {
+            'Meta': {'object_name': 'PollVote'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.PollOption']", 'null': 'True', 'blank': 'True'}),
+            'poll': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'vote_set'", 'to': "orm['misago.Poll']"}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'current_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'delete_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'reports': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            'has_poll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'report_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadprefix': {
+            'Meta': {'object_name': 'ThreadPrefix'},
+            'forums': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Forum']", 'symmetrical': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            '_avatar_crop': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'avatar_crop'", 'blank': 'True'}),
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_vote': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'starter': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.User']"}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 448 - 0
misago/migrations/0027_auto__add_field_thread_prefix.py

@@ -0,0 +1,448 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'Thread.prefix'
+        db.add_column(u'misago_thread', 'prefix',
+                      self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.ThreadPrefix'], null=True, on_delete=models.SET_NULL, blank=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'Thread.prefix'
+        db.delete_column(u'misago_thread', 'prefix_id')
+
+
+    models = {
+        'misago.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"}),
+            'variables': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.ban': {
+            'Meta': {'object_name': 'Ban'},
+            'ban': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'test': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.change': {
+            'Meta': {'object_name': 'Change'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'change': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'post_content': ('django.db.models.fields.TextField', [], {}),
+            'reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'thread_name_new': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread_name_old': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.checkpoint': {
+            'Meta': {'object_name': 'Checkpoint'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'old_forum': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'old_forum_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'old_forum_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.fixture': {
+            'Meta': {'object_name': 'Fixture'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['misago.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'pruned_archive': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Forum']"}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        'misago.forumread': {
+            'Meta': {'object_name': 'ForumRead'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.forumrole': {
+            'Meta': {'object_name': 'ForumRole'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.karma': {
+            'Meta': {'object_name': 'Karma'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.monitoritem': {
+            'Meta': {'object_name': 'MonitorItem'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "'int'", 'max_length': '255'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.newsletter': {
+            'Meta': {'object_name': 'Newsletter'},
+            'content_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'content_plain': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignore_subscriptions': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'progress': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'ranks': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Rank']", 'symmetrical': 'False'}),
+            'step_size': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '32'})
+        },
+        'misago.poll': {
+            'Meta': {'object_name': 'Poll'},
+            '_choices_cache': ('django.db.models.fields.TextField', [], {'db_column': "'choices_cache'"}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            'length': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'max_choices': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'question': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'thread': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'poll_of'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'vote_changing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.polloption': {
+            'Meta': {'object_name': 'PollOption'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'poll': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'option_set'", 'to': "orm['misago.Poll']"}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.pollvote': {
+            'Meta': {'object_name': 'PollVote'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.PollOption']", 'null': 'True', 'blank': 'True'}),
+            'poll': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'vote_set'", 'to': "orm['misago.Poll']"}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'current_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'delete_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'reports': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.pruningpolicy': {
+            'Meta': {'object_name': 'PruningPolicy'},
+            'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_visit': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'registered': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'misago.role': {
+            'Meta': {'object_name': 'Role'},
+            '_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'permissions'", 'blank': 'True'}),
+            '_special': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'special'", 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        },
+        'misago.session': {
+            'Meta': {'object_name': 'Session'},
+            'admin': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'crawler': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'db_column': "'session_data'"}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'matched': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Rank']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"})
+        },
+        'misago.setting': {
+            'Meta': {'object_name': 'Setting'},
+            '_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'db_column': "'value'", 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'extra': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'field': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.SettingsGroup']", 'to_field': "'key'"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'normalize_to': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'separator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'setting': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
+            'value_default': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'misago.settingsgroup': {
+            'Meta': {'object_name': 'SettingsGroup'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.signinattempt': {
+            'Meta': {'object_name': 'SignInAttempt'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
+        },
+        'misago.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            'has_poll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'private_thread_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'prefix': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.ThreadPrefix']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'report_for': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'report_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.threadprefix': {
+            'Meta': {'object_name': 'ThreadPrefix'},
+            'forums': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Forum']", 'symmetrical': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'misago.threadread': {
+            'Meta': {'object_name': 'ThreadRead'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        },
+        'misago.token': {
+            'Meta': {'object_name': 'Token'},
+            'accessed': ('django.db.models.fields.DateTimeField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.CharField', [], {'max_length': '42', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'signin_tokens'", 'to': "orm['misago.User']"})
+        },
+        'misago.user': {
+            'Meta': {'object_name': 'User'},
+            '_avatar_crop': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'db_column': "'avatar_crop'", 'blank': 'True'}),
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_vote': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['misago.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'sync_pds': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'unread_pds': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'misago.usernamechange': {
+            'Meta': {'object_name': 'UsernameChange'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'old_username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'namechanges'", 'to': "orm['misago.User']"})
+        },
+        'misago.watchedthread': {
+            'Meta': {'object_name': 'WatchedThread'},
+            'email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_read': ('django.db.models.fields.DateTimeField', [], {}),
+            'starter': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['misago.User']"}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']"})
+        }
+    }
+
+    complete_apps = ['misago']

+ 1 - 0
misago/models/__init__.py

@@ -21,6 +21,7 @@ from misago.models.settingmodel import Setting
 from misago.models.settingsgroupmodel import SettingsGroup
 from misago.models.signinattemptmodel import SignInAttempt
 from misago.models.threadmodel import Thread
+from misago.models.threadprefixmodel import ThreadPrefix
 from misago.models.threadreadmodel import ThreadRead
 from misago.models.tokenmodel import Token
 from misago.models.usermodel import User, Guest, Crawler

+ 7 - 7
misago/models/forummodel.py

@@ -1,5 +1,4 @@
 import urlparse
-import threading
 from mptt.managers import TreeManager
 from mptt.models import MPTTModel, TreeForeignKey
 from django.conf import settings
@@ -9,8 +8,9 @@ from django.db import models
 from django.db.models import Sum
 from django.utils.translation import ugettext_lazy as _
 from misago.signals import delete_forum_content, move_forum_content, rename_forum, rename_user
+from misago.thread import local
 
-_thread_local = threading.local()
+_thread_local = local()
 
 class ForumManager(TreeManager):
     @property
@@ -116,7 +116,7 @@ class ForumManager(TreeManager):
                         parents[forum.parent_id].last_poster_slug = forum.last_poster_slug
                         parents[forum.parent_id].last_poster_style = forum.last_poster_style
         return forums_list
-    
+
     def ignored_users(self, user, forums):
         check_ids = []
         for forum in forums:
@@ -132,7 +132,7 @@ class ForumManager(TreeManager):
         self.populate_tree()
         readable = []
         for pk, forum in self.forums_tree.items():
-            if ((include_special or not forum.special) and 
+            if ((include_special or not forum.special) and
                     acl.forums.can_browse(forum.pk) and
                     acl.threads.acl[forum.pk]['can_read_threads'] == 2):
                 readable.append(forum.pk)
@@ -142,7 +142,7 @@ class ForumManager(TreeManager):
         self.populate_tree()
         readable = []
         for pk, forum in self.forums_tree.items():
-            if (not forum.special and 
+            if (not forum.special and
                     acl.forums.can_browse(forum.pk) and
                     acl.threads.acl[forum.pk]['can_read_threads'] == 1):
                 readable.append(forum.pk)
@@ -200,11 +200,11 @@ class Forum(MPTTModel):
 
     class Meta:
         app_label = 'misago'
-    
+
     def save(self, *args, **kwargs):
         super(Forum, self).save(*args, **kwargs)
         cache.delete('forums_tree')
-    
+
     def delete(self, *args, **kwargs):
         delete_forum_content.send(sender=self)
         super(Forum, self).delete(*args, **kwargs)

+ 2 - 1
misago/models/threadmodel.py

@@ -5,7 +5,7 @@ from django.db.models.signals import pre_save, pre_delete
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from misago.signals import (delete_user_content, merge_thread, move_forum_content,
-                            move_thread, rename_user, sync_user_profile)
+                            move_thread, rename_user, sync_user_profile, remove_thread_prefix)
 from misago.utils.strings import slugify
 
 class ThreadManager(models.Manager):
@@ -51,6 +51,7 @@ class ThreadManager(models.Manager):
 class Thread(models.Model):
     forum = models.ForeignKey('Forum')
     weight = models.PositiveIntegerField(default=0)
+    prefix = models.ForeignKey('ThreadPrefix', null=True, blank=True, on_delete=models.SET_NULL)
     name = models.CharField(max_length=255)
     slug = models.SlugField(max_length=255)
     replies = models.PositiveIntegerField(default=0)

+ 100 - 0
misago/models/threadprefixmodel.py

@@ -0,0 +1,100 @@
+from django.core.cache import cache
+from django.db import models
+from django.utils.datastructures import SortedDict
+from misago.signals import (merge_thread, move_forum_content,
+                            move_thread, remove_thread_prefix)
+from misago.thread import local
+
+_thread_local = local()
+
+class ThreadPrefixManager(models.Manager):
+    def flush_cache(self):
+        cache.delete('threads_prefixes')
+
+    def get_cache(self):
+        try:
+            return _thread_local.misago_thread_prefixes
+        except AttributeError:
+            _thread_local.misago_thread_prefixes = self.make_cache()
+        return _thread_local.misago_thread_prefixes
+
+    def make_cache(self):
+        raw_prefixes = cache.get('threads_prefixes', 'nada')
+        if raw_prefixes == 'nada':
+            raw_prefixes = [p for p in ThreadPrefix.objects.prefetch_related().order_by('name')]
+            cache.set('threads_prefixes', raw_prefixes, None)
+        dict_result = SortedDict()
+        for prefix in raw_prefixes:
+            dict_result[prefix.pk] = prefix
+        return dict_result
+
+    def forum_prefixes(self, forum):
+        forum_prefixes = []
+        for prefix in self.get_cache().values():
+            if forum in prefix.forums.all():
+                forum_prefixes.append((prefix.pk, prefix))
+        return SortedDict(forum_prefixes)
+
+    def prefix_in_forum(self, prefix, forum):
+        forum_prefixes = self.forum_prefixes(forum)
+        return prefix in forum_prefixes
+
+
+class ThreadPrefix(models.Model):
+    forums = models.ManyToManyField('Forum')
+    name = models.CharField(max_length=255)
+    slug = models.SlugField(max_length=255)
+    style = models.CharField(max_length=255)
+
+    objects = ThreadPrefixManager()
+
+    class Meta:
+        app_label = 'misago'
+
+    def save(self, *args, **kwargs):
+        ThreadPrefix.objects.flush_cache()
+        super(ThreadPrefix, self).save(*args, **kwargs)
+
+    def delete(self, *args, **kwargs):
+        ThreadPrefix.objects.flush_cache()
+        super(ThreadPrefix, self).delete(*args, **kwargs)
+
+    def update_forums(self, new_forums):
+        current_forums = self.forums.all()
+
+        removed_forums = []
+        for forum in current_forums:
+            if not forum in new_forums:
+                removed_forums.append(forum)
+
+        if removed_forums:
+            remove_thread_prefix.send(sender=self, forums=removed_forums)
+
+        self.forums.clear()
+        for forum in new_forums:
+            self.forums.add(forum)
+
+
+def move_forum_content_handler(sender, **kwargs):
+    old_forum_prefixes = ThreadPrefix.objects.forum_prefixes(sender)
+    new_forum_prefixes = ThreadPrefix.objects.forum_prefixes(kwargs['move_to'])
+    bad_prefixes = list(set(new_forum_prefixes) - set(old_forum_prefixes))
+
+    if bad_prefixes:
+        sender.thread_set.filter(prefix__in=bad_prefixes).update(prefix=None)
+
+move_forum_content.connect(move_forum_content_handler, dispatch_uid="move_forum_threads_prefixes")
+
+
+def move_thread_handler(sender, **kwargs):
+    if sender.prefix and not ThreadPrefix.objects.prefix_in_forum(sender.prefix, kwargs['move_to']):
+        sender.prefix = None
+
+move_thread.connect(move_thread_handler, dispatch_uid="move_thread_prefixes")
+
+
+def merge_thread_handler(sender, **kwargs):
+    if kwargs['new_thread'].prefix and not ThreadPrefix.objects.prefix_in_forum(kwargs['new_thread'].prefix, kwargs['new_thread'].forum):
+        kwargs['new_thread'].prefix = None
+
+merge_thread.connect(merge_thread_handler, dispatch_uid="merge_threads_prefixes")

+ 2 - 1
misago/signals.py

@@ -9,4 +9,5 @@ move_post = django.dispatch.Signal(providing_args=["move_to"])
 move_thread = django.dispatch.Signal(providing_args=["move_to"])
 rename_forum = django.dispatch.Signal()
 rename_user = django.dispatch.Signal()
-sync_user_profile = django.dispatch.Signal()
+sync_user_profile = django.dispatch.Signal()
+remove_thread_prefix = django.dispatch.Signal(providing_args=["forums"])

+ 12 - 1
static/cranefly/css/cranefly.css

@@ -980,7 +980,7 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{color:#333;text-decoration:n
 .forum-subforums-list.forum-subforums-important .header{background-color:#cf402e;border:1px solid #a53325}.forum-subforums-list.forum-subforums-important .header h2{color:#fff;text-shadow:0 1px 0 #672017}.forum-subforums-list.forum-subforums-important .header h2 small{color:#280c09;text-shadow:none}
 .forum-subforums-list.forum-subforums-inverse .header{background-color:#333;border:1px solid #1a1a1a}.forum-subforums-list.forum-subforums-inverse .header h2{color:#eee;text-shadow:0 1px 0 #000}.forum-subforums-list.forum-subforums-inverse .header h2 small{color:#b3b3b3;text-shadow:none}
 .forum-subforums-list.forum-subforums-info .header{background-color:#3c85a3;border:1px solid #2e677e}.forum-subforums-list.forum-subforums-info .header h2{color:#fff;text-shadow:0 1px 0 #1a3946}.forum-subforums-list.forum-subforums-info .header h2 small{color:#1a3946;text-shadow:none}
-.forum-threads-list{background-color:#fff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0 0 0 3px #eee;-moz-box-shadow:0 0 0 3px #eee;box-shadow:0 0 0 3px #eee;margin-bottom:20px}.forum-threads-list .threads-list-empty{padding:28px 0;color:#999;font-size:26.25px;text-align:center}
+.forum-threads-list{background-color:#fff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0 0 0 3px #eee;-moz-box-shadow:0 0 0 3px #eee;box-shadow:0 0 0 3px #eee;margin-bottom:20px}.forum-threads-list .threads-list-empty{padding:28px 0;color:#999;font-size:26.25px;text-align:center}.forum-threads-list .threads-list-empty .thread-prefix{position:relative;bottom:5px}
 .forum-threads-list>ul{list-style:none;margin:0}.forum-threads-list>ul>li{border-bottom:1px solid #d5d5d5;padding:9.9px 0;overflow:auto}.forum-threads-list>ul>li .thread-icon{float:left;margin-bottom:-1px;height:22px;width:39.5px;font-size:22px;text-align:center}.forum-threads-list>ul>li .thread-icon a:link,.forum-threads-list>ul>li .thread-icon a:active,.forum-threads-list>ul>li .thread-icon a:visited,.forum-threads-list>ul>li .thread-icon a:hover{color:#d5d5d5;text-decoration:none}
 .forum-threads-list>ul>li .thread-body{margin:-9.9px 0;margin-left:39.5px;padding:9.9px 0}.forum-threads-list>ul>li .thread-body .thread-title{display:block}.forum-threads-list>ul>li .thread-body .thread-title a:link,.forum-threads-list>ul>li .thread-body .thread-title a:active,.forum-threads-list>ul>li .thread-body .thread-title a:visited,.forum-threads-list>ul>li .thread-body .thread-title a:hover{color:#555;font-size:17.5px}
 .forum-threads-list>ul>li .thread-body .thread-activity{float:right;list-style:none;margin:0;margin-top:-9.9px;margin-bottom:-6px;padding:6px 0;padding-right:8px;overflow:auto;position:relative;top:1px;z-index:2}.forum-threads-list>ul>li .thread-body .thread-activity li{float:left;margin:0;margin-left:10px;padding:0}.forum-threads-list>ul>li .thread-body .thread-activity li.thread-replies,.forum-threads-list>ul>li .thread-body .thread-activity li.thread-replies-reported,.forum-threads-list>ul>li .thread-body .thread-activity li.thread-replies-moderated{position:relative;top:2px;color:#555;font-size:17.5px;font-weight:bold}
@@ -1009,6 +1009,17 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{color:#333;text-decoration:n
 .forum-threads-list .threads-actions{background-color:#fbfbfb;border-top:1px solid #d5d5d5;border-radius:0 0 2px 2px;overflow:auto;padding:4px;color:#999;font-size:11.9px}.forum-threads-list .threads-actions form{margin-bottom:0}
 .forum-threads-extra{overflow:auto}.forum-threads-extra.extra-top{margin-bottom:20px}
 .forum-threads-extra .threads-signin-message{float:right}.forum-threads-extra .threads-signin-message a:link,.forum-threads-extra .threads-signin-message a:visited{color:#333}
+.form-prefix{margin:0;padding:0}.form-prefix .btn{background-color:#d4d4d4;border-color:#a2a2a2;opacity:.9;filter:alpha(opacity=90);color:#555;text-shadow:none}.form-prefix .btn.btn-active{background-color:#555;border-color:#222;color:#fff;text-shadow:0 1px 1px #222}
+.form-prefix .btn.btn-cancel{background-color:#fff;border-color:#b3b3b3}.form-prefix .btn.btn-cancel:hover,.form-prefix .btn.btn-cancel:active{background-color:#555;border-color:#222;color:#fff;text-shadow:0 1px 1px #222}
+.form-prefix .btn.btn-success,.form-prefix .btn.btn-success:hover,.form-prefix .btn.btn-success:active{background-color:#cdeacd;border-color:#86cb86;color:#46a546}
+.form-prefix .btn.btn-success.btn-active{background-color:#46a546;border-color:#285d28;color:#fff;text-shadow:0 1px 1px #285d28}
+.form-prefix .btn.btn-info,.form-prefix .btn.btn-info:hover,.form-prefix .btn.btn-info:active{background-color:#ade6fe;border-color:#49c7fc;color:#049cdb}
+.form-prefix .btn.btn-info.btn-active{background-color:#049cdb;border-color:#025577;color:#fff;text-shadow:0 1px 1px #025577}
+.form-prefix .btn.btn-warning,.form-prefix .btn.btn-warning:hover,.form-prefix .btn.btn-warning:active{background-color:#fee9cc;border-color:#fbbf69;color:#f89406}
+.form-prefix .btn.btn-warning.btn-active{background-color:#f89406;border-color:#945904;color:#fff;text-shadow:0 1px 1px #945904}
+.form-prefix .btn.btn-danger,.form-prefix .btn.btn-danger:hover,.form-prefix .btn.btn-danger:active{background-color:#f5d7d4;border-color:#e38b80;color:#cf402e}
+.form-prefix .btn.btn-danger.btn-active{background-color:#cf402e;border-color:#7c261b;color:#fff;text-shadow:0 1px 1px #7c261b}
+.form-prefix .btn.btn-active,.form-prefix .btn:hover,.form-prefix .btn:active{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;opacity:1;filter:alpha(opacity=100)}
 .thread-buttons{overflow:auto}.thread-buttons .pull-right{margin-left:14px}
 .thread-buttons .thread-signin-message{float:right}.thread-buttons .thread-signin-message a:link,.thread-buttons .thread-signin-message a:visited{color:#333}
 .thread-poll-body{background-color:#fff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0 0 0 3px #eee;-moz-box-shadow:0 0 0 3px #eee;box-shadow:0 0 0 3px #eee;margin-bottom:20px;padding:10px 20px}.thread-poll-body h2{margin-left:180px;margin-bottom:20px}

+ 113 - 0
static/cranefly/css/cranefly/forum.less

@@ -311,6 +311,11 @@
     color: @grayLight;
     font-size: @fontSizeLarge * 1.5;
     text-align: center;
+
+    .thread-prefix {
+      position: relative;
+      bottom: 5px;
+    }
   }
 
   &>ul {
@@ -590,4 +595,112 @@
       color: @textColor;
     }
   }
+}
+
+.form-prefix {
+  margin: 0px;
+  padding: 0px;
+
+  .btn {
+    background-color: lighten(@gray, 50%);
+    border-color: lighten(@gray, 30%);
+    .opacity(90);
+
+    color: @gray;
+    text-shadow: none;
+
+    &.btn-active {
+      background-color: @gray;
+      border-color: darken(@gray, 20%);
+
+      color: @white;
+      text-shadow: 0px 1px 1px darken(@gray, 20%);
+    }
+
+    &.btn-cancel {
+      background-color: @white;
+      border-color: darken(@white, 30%);
+
+      &:hover, &:active {
+        background-color: @gray;
+        border-color: darken(@gray, 20%);
+
+        color: @white;
+        text-shadow: 0px 1px 1px darken(@gray, 20%);
+      }
+    }
+
+    &.btn-success {
+      &, &:hover, &:active {
+        background-color: lighten(@green, 40%);
+        border-color: lighten(@green, 20%);
+
+        color: @green;
+      }
+
+      &.btn-active {
+        background-color: @green;
+        border-color: darken(@green, 20%);
+
+        color: @white;
+        text-shadow: 0px 1px 1px darken(@green, 20%);
+      }
+    }
+
+    &.btn-info {
+      &, &:hover, &:active {
+        background-color: lighten(@blue, 40%);
+        border-color: lighten(@blue, 20%);
+
+        color: @blue;
+      }
+
+      &.btn-active {
+        background-color: @blue;
+        border-color: darken(@blue, 20%);
+
+        color: @white;
+        text-shadow: 0px 1px 1px darken(@blue, 20%);
+      }
+    }
+
+    &.btn-warning {
+      &, &:hover, &:active {
+        background-color: lighten(@orange, 40%);
+        border-color: lighten(@orange, 20%);
+
+        color: @orange;
+      }
+
+      &.btn-active {
+        background-color: @orange;
+        border-color: darken(@orange, 20%);
+
+        color: @white;
+        text-shadow: 0px 1px 1px darken(@orange, 20%);
+      }
+    }
+
+    &.btn-danger {
+      &, &:hover, &:active {
+        background-color: lighten(@red, 40%);
+        border-color: lighten(@red, 20%);
+
+        color: @red;
+      }
+
+      &.btn-active {
+        background-color: @red;
+        border-color: darken(@red, 20%);
+
+        color: @white;
+        text-shadow: 0px 1px 1px darken(@red, 20%);
+      }
+    }
+
+    &.btn-active, &:hover, &:active {
+      .box-shadow(none);
+      .opacity(100);
+    }
+  }
 }

+ 8 - 0
templates/admin/prefixes/form.html

@@ -0,0 +1,8 @@
+{% extends "admin/admin/form.html" %}
+{% import "forms.html" as form_theme with context %}
+
+{% block form %}
+{{ form_theme.row(form.name, attrs={'class': 'span12'}) }}
+{{ form_theme.row(form.style, attrs={'class': 'span12'}) }}
+{{ form_theme.row(form.forums, attrs={'class': 'span12'}) }}
+{% endblock %}

+ 7 - 0
templates/admin/prefixes/list.html

@@ -0,0 +1,7 @@
+{% extends "admin/admin/list.html" %}
+
+{% block table_row scoped %}
+  <td class="lead-cell">
+    <strong>{{ _(item.name) }}</strong> <span class="muted">{{ item.style }}</span>
+  </td>
+{% endblock%}

+ 28 - 0
templates/cranefly/threads/list.html

@@ -231,7 +231,11 @@
     {% endif %}
     {% else %}
     <div class="threads-list-empty">
+      {% if active_prefix %}
+      {% trans prefix=draw_prefix(active_prefix) %}There are no threads prefixed with {{ prefix }} in this forum.{% endtrans %}
+      {% else %}
       {% trans %}There are no threads in this forum.{% endtrans %}
+      {% endif %}
     </div>
     {% endif %}
   </div>
@@ -288,11 +292,35 @@
     {%- if pagination['prev'] > 1 %}<li><a href="{{ url('forum', slug=forum.slug, forum=forum.id) }}" class="tooltip-top" title="{% trans %}First Page{% endtrans %}"><i class="icon-chevron-left"></i> {% trans %}First{% endtrans %}</a></li>{% endif -%}
     {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{{ url('forum', slug=forum.slug, forum=forum.id, page=pagination['prev']) }}{% else %}{{ url('forum', slug=forum.slug, forum=forum.id) }}{% endif %}" class="tooltip-top" title="{% trans %}Newest Threads{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
     {%- if pagination['next'] > 0 %}<li><a href="{{ url('forum', slug=forum.slug, forum=forum.id, page=pagination['next']) }}" class="tooltip-top" title="{% trans %}Older Threads{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+    {% if prefixes and active_prefix %}
+    <li>
+      <form class="form-prefix" action="{{ url('forum_switch_prefix', slug=forum.slug, forum=forum.id) }}" method="POST">
+        <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+        <input type="hidden" name="retreat" value="{{ request_path }}">
+        <input type="hidden" name="switch_prefix" value="0">
+        <button type="submit" class="btn btn-cancel">{% trans %}All Threads{% endtrans %}</button>
+      </form>
+    </li>
+    {% endif %}
+    {% for prefix in prefixes.values() %}
+    <li>
+      <form class="form-prefix" action="{{ url('forum_switch_prefix', slug=forum.slug, forum=forum.id) }}" method="POST">
+        <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+        <input type="hidden" name="retreat" value="{{ request_path }}">
+        <input type="hidden" name="switch_prefix" value="{% if active_prefix == prefix %}0{% else %}{{ prefix.pk }}{% endif %}">
+        <button type="submit" class="btn{% if prefix.style %} btn-{{ prefix.style }}{% endif %}{% if active_prefix == prefix %} btn-active{% endif %}">{{ _(prefix.name) }}</button>
+      </form>
+    </li>
+    {% endfor %}
   </ul>
 </div>
 {% endif %}
 {% endmacro %}
 
+{% macro draw_prefix(prefix) -%}
+<span class="label{% if prefix.style %} label-{{ prefix.style }}{% endif %} thread-prefix">{{ _(prefix.name) }}</span>
+{%- endmacro %}
+
 {% block javascripts -%}{{ super() }}
   <script type="text/javascript">
     $(function () {