Browse Source

Add poll to new thread. #28

Rafał Pitoń 11 years ago
parent
commit
b01f4c3cbc

+ 14 - 0
misago/acl/permissions/threads.py

@@ -561,6 +561,20 @@ class ThreadsACL(BaseACL):
         except KeyError:
             raise ACLError403(_("You don't have permission to see who voted on this post."))
 
+    def can_make_polls(self, forum):
+        try:
+            forum_role = self.get_role(forum)
+            return forum_role['can_make_polls']
+        except KeyError:
+            return False
+
+    def can_vote_in_polls(self, forum):
+        try:
+            forum_role = self.get_role(forum)
+            return forum_role['can_vote_in_polls']
+        except KeyError:
+            return False
+
     def can_see_all_checkpoints(self, forum):
         try:
             forum_role = self.get_role(forum)

+ 109 - 0
misago/apps/threads/forms.py

@@ -0,0 +1,109 @@
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.apps.threadtype.posting.forms import NewThreadForm as NewThreadFormBase
+from misago.forms import Form
+from misago.validators import validate_sluggable
+
+class NewThreadForm(NewThreadFormBase):
+    def type_fields(self):
+        if self.request.acl.threads.can_make_polls(self.forum):
+            self.add_field('poll_question',
+                           forms.CharField(label=_("Poll Question"),
+                                           required=False))
+            self.add_field('poll_choices',
+                           forms.CharField(label=_("Poll Choices"),
+                                           help_text=_("Enter options poll members will vote on, every one in new line."),
+                                           required=False,
+                                           widget=forms.Textarea))
+            self.add_field('poll_max_choices',
+                           forms.IntegerField(label=_("Choices Per User"),
+                                              help_text=_("Select on how many options individual user will be able to vote on."),
+                                              min_value=1,
+                                              initial=1))
+            self.add_field('poll_length',
+                           forms.IntegerField(label=_("Poll Length"),
+                                              help_text=_("Number of days since poll creations users will be allowed to vote in poll. Enter zero for permanent poll."),
+                                              min_value=0,
+                                              initial=0))
+            self.add_field('poll_public',
+                           forms.BooleanField(label=_("Public Voting"),
+                                              required=False))
+            self.add_field('poll_changing_votes',
+                           forms.BooleanField(label=_("Allow Changing Votes"),
+                                              required=False))
+
+    def clean_poll_question(self):
+        data = self.cleaned_data['poll_question'].strip()
+        if data:
+            if len(data) < 3:
+                raise forms.ValidationError(_("Poll quesiton should be at least three characters long."))
+            if len(data) > 255:
+                raise forms.ValidationError(_("Poll quesiton should be no longer than 250 characters."))
+        return data
+
+    def clean_poll_choices(self):
+        self.clean_choices = []
+        data = self.cleaned_data['poll_choices']
+
+        if data:
+            for choice in data.splitlines():
+                choice = choice.strip()
+                if not choice in self.clean_choices:
+                    if len(choice) < 3:
+                        raise forms.ValidationError(_("Poll choices should be at least three characters long."))
+                    if len(choice) > 250:
+                        raise forms.ValidationError(_("Poll choices should be no longer than 250 characters."))
+                    self.clean_choices.append(choice)
+            if len(self.clean_choices) < 2:
+                raise forms.ValidationError(_("Poll needs at least two choices."))
+            if len(self.clean_choices) > 10:
+                raise forms.ValidationError(_("Poll cannot have more than 10 choices."))
+
+        return '\r\n'.join(self.clean_choices)
+
+    def clean_poll_max_choices(self):
+        data = self.cleaned_data['poll_max_choices']
+        if data < 1:
+            raise forms.ValidationError(_("Voters must be allowed to make at least one choice."))
+        if data > len(self.clean_choices):
+            raise forms.ValidationError(_("Users cannot cast more votes than there are options."))
+        return data
+
+    def clean_poll_length(self):
+        data = self.cleaned_data['poll_length']
+        if data < 0:
+            raise forms.ValidationError(_("Poll length cannot be negative."))
+        if data > 300:
+            raise forms.ValidationError(_("Poll length cannot be longer than 300 days."))
+        return data
+
+    def clean(self):
+        data = super(NewThreadForm, self).clean()
+        try:
+            if bool(data['poll_question']) != bool(self.clean_choices):
+                if bool(data['poll_question']):
+                    raise forms.ValidationError(_("You have to define poll choices."))
+                else:
+                    raise forms.ValidationError(_("You have to define poll question."))
+        except KeyError:
+            pass
+        return data
+
+
+class PollVoteForm(Form):
+    def __init__(self, *args, **kwargs):
+        self.poll = kwargs.pop('poll')
+        super(PollVoteForm, self).__init__(*args, **kwargs)
+
+    def finalize_form(self):
+        choices = []
+        for choice in self.poll.option_set.all():
+            choices.append((choice.pk, choice.name))
+        if self.poll.max_choices > 1:
+            self.add_field('options',
+                           forms.TypedMultipleChoiceField(choices=choices, coerce=int,
+                                                          widget=forms.CheckboxSelectMultiple))
+        else:
+            self.add_field('options',
+                           forms.TypedChoiceField(choices=choices, coerce=int,
+                                                  widget=forms.RadioSelect))

+ 35 - 1
misago/apps/threads/posting.py

@@ -1,15 +1,47 @@
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
+from django.utils import timezone
 from django.utils.translation import ugettext as _
 from misago import messages
+from misago.apps.threads.forms import NewThreadForm
 from misago.apps.threadtype.posting import NewThreadBaseView, EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
-from misago.models import Forum, Thread, Post
+from misago.models import Forum, Thread, Post, Poll, PollOption
 from misago.apps.threads.mixins import TypeMixin
 
 class NewThreadView(NewThreadBaseView, TypeMixin):
+    form_type = NewThreadForm
+
     def set_forum_context(self):
         self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
 
+    def after_form(self, form):
+        poll = Poll(forum=self.forum,
+                    thread=self.thread,
+                    user=self.request.user,
+                    start_date=timezone.now(),
+                    length=form.cleaned_data['poll_length'],
+                    question=form.cleaned_data['poll_question'],
+                    max_choices=form.cleaned_data['poll_max_choices'],
+                    vote_changing=form.cleaned_data['poll_changing_votes'],
+                    public=form.cleaned_data['poll_max_choices'])
+        poll.save()
+
+        choices = []
+        for name in form.clean_choices:
+            option = PollOption.objects.create(
+                                               poll=poll,
+                                               forum=self.forum,
+                                               thread=self.thread,
+                                               name=name,
+                                               )
+            choices.append(option)
+
+        poll.choices_cache = choices
+        poll.save()
+
+        self.thread.has_poll = True
+        self.thread.save()
+
     def response(self):
         if self.post.moderated:
             messages.success(self.request, _("New thread has been posted. It will be hidden from other members until moderator reviews it."), 'threads')
@@ -19,6 +51,8 @@ class NewThreadView(NewThreadBaseView, TypeMixin):
 
 
 class EditThreadView(EditThreadBaseView, TypeMixin):
+    form_type = NewThreadForm
+
     def response(self):
         messages.success(self.request, _("Your thread has been edited."), 'threads_%s' % self.post.pk)
         return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))

+ 17 - 0
misago/apps/threads/thread.py

@@ -1,9 +1,26 @@
 from django.utils.translation import ugettext as _
 from misago.apps.threadtype.thread import ThreadBaseView, ThreadModeration, PostsModeration
 from misago.models import Forum, Thread
+from misago.apps.threads.forms import PollVoteForm
 from misago.apps.threads.mixins import TypeMixin
 
 class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
+    def template_vars(self, context):
+        context['poll'] = None
+        context['poll_form'] = None
+        if self.thread.has_poll:
+            context['poll'] = self.thread.poll
+            self.thread.poll.option_set.all()
+            if self.request.user.is_authenticated():
+                self.thread.poll.user_votes = self.request.user.pollvote_set.filter(poll=self.thread.poll)
+                if (not self.thread.closed
+                        and not self.thread.deleted
+                        and self.request.acl.threads.can_vote_in_polls(self.forum)
+                        and not self.thread.poll.over
+                        and (self.thread.poll.vote_changing or not self.thread.poll.user_votes)):
+                    context['poll_form'] = PollVoteForm(poll=self.thread.poll)
+        return super(ThreadView, self).template_vars(context)
+
     def posts_actions(self):
         acl = self.request.acl.threads.get_role(self.thread.forum_id)
         actions = []

+ 485 - 0
misago/migrations/0022_auto__add_polloption__add_poll__add_pollvote__add_field_thread_has_pol.py

@@ -0,0 +1,485 @@
+# -*- 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 'PollOption'
+        db.create_table(u'misago_polloption', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('poll', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.Poll'])),
+            ('forum', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.Forum'])),
+            ('thread', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.Thread'])),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('votes', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
+        ))
+        db.send_create_signal('misago', ['PollOption'])
+
+        # Adding model 'Poll'
+        db.create_table(u'misago_poll', (
+            ('forum', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.Forum'])),
+            ('thread', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['misago.Thread'], unique=True, primary_key=True)),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.User'], null=True, on_delete=models.SET_NULL, blank=True)),
+            ('start_date', self.gf('django.db.models.fields.DateTimeField')()),
+            ('length', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
+            ('question', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('max_choices', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
+            ('_choices_cache', self.gf('django.db.models.fields.TextField')(db_column='choices_cache')),
+            ('votes', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
+            ('vote_changing', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('public', self.gf('django.db.models.fields.BooleanField')(default=False)),
+        ))
+        db.send_create_signal('misago', ['Poll'])
+
+        # Adding model 'PollVote'
+        db.create_table(u'misago_pollvote', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('poll', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.Poll'])),
+            ('forum', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.Forum'])),
+            ('thread', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.Thread'])),
+            ('option', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.PollOption'])),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.User'], null=True, on_delete=models.SET_NULL, blank=True)),
+            ('date', self.gf('django.db.models.fields.DateTimeField')()),
+            ('ip', self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39)),
+            ('agent', self.gf('django.db.models.fields.CharField')(max_length=255)),
+        ))
+        db.send_create_signal('misago', ['PollVote'])
+
+        # Adding field 'Thread.has_poll'
+        db.add_column(u'misago_thread', 'has_poll',
+                      self.gf('django.db.models.fields.BooleanField')(default=False),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting model 'PollOption'
+        db.delete_table(u'misago_polloption')
+
+        # Deleting model 'Poll'
+        db.delete_table(u'misago_poll')
+
+        # Deleting model 'PollVote'
+        db.delete_table(u'misago_pollvote')
+
+        # Deleting field 'Thread.has_poll'
+        db.delete_column(u'misago_thread', 'has_poll')
+
+
+    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', [], {'to': "orm['misago.Thread']", 'unique': 'True', 'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', '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', [], {'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']"}),
+            'poll': ('django.db.models.fields.related.ForeignKey', [], {'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'})
+        },
+        '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.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']

+ 3 - 0
misago/models/__init__.py

@@ -9,6 +9,9 @@ from misago.models.forumrolemodel import ForumRole
 from misago.models.karmamodel import Karma
 from misago.models.monitoritemmodel import MonitorItem
 from misago.models.newslettermodel import Newsletter
+from misago.models.pollmodel import Poll
+from misago.models.polloptionmodel import PollOption
+from misago.models.pollvotemodel import PollVote
 from misago.models.postmodel import Post
 from misago.models.pruningpolicymodel import PruningPolicy
 from misago.models.rankmodel import Rank

+ 55 - 0
misago/models/pollmodel.py

@@ -0,0 +1,55 @@
+from datetime import timedelta
+from django.db import models
+from django.utils import timezone
+import base64
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+class Poll(models.Model):
+    forum = models.ForeignKey('Forum')
+    thread = models.OneToOneField('Thread', primary_key=True)
+    user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
+    start_date = models.DateTimeField()
+    length = models.PositiveIntegerField(default=0)
+    question = models.CharField(max_length=255)
+    max_choices = models.PositiveIntegerField(default=0)
+    _choices_cache = models.TextField(db_column='choices_cache')
+    votes = models.PositiveIntegerField(default=0)
+    vote_changing = models.BooleanField(default=False)
+    public = models.BooleanField(default=False)
+
+    class Meta:
+        app_label = 'misago'
+
+    @property
+    def end_date(self):
+        return self.start_date + timedelta(days=self.length)
+
+    @property
+    def over(self):
+        if not self.length:
+            return False
+        return timezone.now() > self.end_date
+
+    @property
+    def choices_cache(self):
+        if self._cache:
+            return self._cache
+
+        try:
+            self._cache = pickle.loads(base64.decodestring(self._choices_cache))
+        except Exception:
+            self._cache = {}
+
+        return self._cache
+
+    @choices_cache.setter
+    def choices_cache(self, choices):
+        choices_cache = {'order': [], 'choices': {}}
+        for choice in choices:
+            choices_cache['order'].append(choice.pk)
+            choices_cache['choices'][choice.pk] = choice
+        self._cache = choices_cache
+        self._choices_cache = base64.encodestring(pickle.dumps(choices_cache, pickle.HIGHEST_PROTOCOL))

+ 11 - 0
misago/models/polloptionmodel.py

@@ -0,0 +1,11 @@
+from django.db import models
+
+class PollOption(models.Model):
+    poll = models.ForeignKey('Poll', related_name="option_set")
+    forum = models.ForeignKey('Forum')
+    thread = models.ForeignKey('Thread')
+    name = models.CharField(max_length=255)
+    votes = models.PositiveIntegerField(default=0)
+
+    class Meta:
+        app_label = 'misago'

+ 14 - 0
misago/models/pollvotemodel.py

@@ -0,0 +1,14 @@
+from django.db import models
+
+class PollVote(models.Model):
+    poll = models.ForeignKey('Poll')
+    forum = models.ForeignKey('Forum')
+    thread = models.ForeignKey('Thread')
+    option = models.ForeignKey('PollOption')
+    user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
+    date = models.DateTimeField()
+    ip = models.GenericIPAddressField()
+    agent = models.CharField(max_length=255)
+
+    class Meta:
+        app_label = 'misago'

+ 3 - 2
misago/models/threadmodel.py

@@ -39,7 +39,7 @@ class ThreadManager(models.Manager):
         if user.is_authenticated():
             for read in ThreadRead.objects.filter(user=user).filter(thread__in=threads_dict.keys()):
                 try:
-                    threads_dict[read.thread_id].is_read = (threads_dict[read.thread_id].last <= cutoff or 
+                    threads_dict[read.thread_id].is_read = (threads_dict[read.thread_id].last <= cutoff or
                                                             threads_dict[read.thread_id].last <= read.updated or
                                                             threads_dict[read.thread_id].last <= forum_reads[read.forum_id])
                 except KeyError:
@@ -74,6 +74,7 @@ class Thread(models.Model):
     last_poster_style = models.CharField(max_length=255, null=True, blank=True)
     participants = models.ManyToManyField('User', related_name='private_thread_set')
     report_for = models.ForeignKey('Post', related_name='report_set', null=True, blank=True, on_delete=models.SET_NULL)
+    has_poll = models.BooleanField(default=False)
     moderated = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     closed = models.BooleanField(default=False)
@@ -201,7 +202,7 @@ class Thread(models.Model):
         # Flags
         self.moderated = start_post.moderated
         self.deleted = start_post.deleted
-        
+
     def email_watchers(self, request, thread_type, post):
         from misago.acl.builder import acl
         from misago.acl.exceptions import ACLError403, ACLError404

+ 40 - 1
misago/templatetags/utils.py

@@ -22,4 +22,43 @@ def make_short(string, length=16):
 def highlight_result(text, query, length=500):
     hl = Highlighter(query, html_tag='strong', max_length=length)
     hl = hl.highlight(text)
-    return hl
+    return hl
+
+
+@register.global_function(name='color')
+def color_wheel(index):
+    while index > 15:
+        index -= 15
+    colors = (
+              (49, 130, 189),
+              (49, 163, 84),
+              (230, 85, 13),
+              (117, 107, 177),
+              (222, 45, 38),
+              (158, 202, 225),
+              (161, 217, 155),
+              (253, 174, 107),
+              (188, 189, 220),
+              (252, 146, 114),
+              (222, 235, 247),
+              (229, 245, 224),
+              (254, 230, 206),
+              (239, 237, 245),
+              (254, 224, 210),
+             )
+    return colors[index]
+
+
+@register.global_function(name='colorhex')
+def color_hex(index):
+    color = color_wheel(index)
+    r = unicode(hex(color[0])[2:])
+    if len(r) == 0:
+        r = '0%s' % r
+    g = unicode(hex(color[1])[2:])
+    if len(g) == 0:
+        g = '0%s' % g
+    b = unicode(hex(color[2])[2:])
+    if len(b) == 0:
+        b = '0%s' % b
+    return r+g+b

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

@@ -1008,6 +1008,7 @@ 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}
+.thread-poll{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:20px}
 .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-body .post-wrapper .post-body{margin-bottom:20px;overflow:auto}.thread-body .post-wrapper .post-body .user-avatar{border-radius:5px;float:left;width:100px;height:100px}

+ 9 - 0
static/cranefly/css/cranefly/thread.less

@@ -1,6 +1,15 @@
 // Thread view
 // -------------------------
 
+.thread-poll {
+  background-color: @categoryBackground;
+  border: 1px solid @categoryBorder;
+  border-radius: @borderRadiusSmall;
+  .box-shadow(0px 0px 0px 3px @categoryShadow);
+  margin-bottom: @baseLineHeight;
+  padding: @baseLineHeight;
+}
+
 .thread-buttons {
   overflow: auto;
 

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

@@ -181,6 +181,11 @@
               {% if settings.avatars_on_threads_list %}<img src="{{ macros.avatar_guest(24) }}" alt="{{ thread.last_poster_name }}" class="user-avatar">{% else %}{{ thread.last_poster_name }}{% endif %}
               {% endif %}
             </li>
+            {% if thread.has_poll %}
+            <li class="thread-flag thread-poll tooltip-top" title="{% trans %}This thread is poll{% endtrans %}">
+              <i class="icon-bar-chart"></i>
+            </li>
+            {% endif %}
             {% if thread.moderated %}
             <li class="thread-flag thread-moderated tooltip-top" title="{% trans %}This thread awaits review{% endtrans %}">
               <i class="icon-remove-circle"></i>

+ 24 - 4
templates/cranefly/threads/posting.html

@@ -65,13 +65,24 @@
             <h4>{% trans %}Message Body{% endtrans %}</h4>
             {% endif %}
             {{ editor.editor(form.post, get_button(), rows=8, zen=True, extra=get_extra()) }}
-            {% if intersect(form.fields, ('edit_reason', 'thread_weight', 'close_thread')) %}
-            <hr>
+
             {% if 'edit_reason' in form.fields %}
             {{ form_theme.row(form.edit_reason, attrs={'class': 'span8'}) }}
             {% endif %}
 
+            {% if 'poll_question' in form.fields %}
+            <hr>
+            <h4>{% trans %}Create Poll{% endtrans %}</h4>
+            {{ form_theme.row(form.poll_question, attrs={'class': 'span8'}) }}
+            {{ form_theme.row(form.poll_choices, attrs={'class': 'span8', 'rows': 4}) }}
+            {{ form_theme.row(form.poll_max_choices, attrs={'class': 'span8'}) }}
+            {{ form_theme.row(form.poll_length, attrs={'class': 'span8'}) }}
+            {{ form_theme.row(form.poll_public, attrs={'inline': lang_poll_public()}) }}
+            {{ form_theme.row(form.poll_changing_votes, attrs={'inline': lang_poll_changing_votes()}) }}
+            {% endif %}
+
             {% if intersect(form.fields, ('thread_weight', 'close_thread')) %}
+            <hr>
             <div class="control-group">
               <label class="control-label">{% trans %}Thread Status{% endtrans %}:</label>
               <div class="controls">
@@ -96,7 +107,6 @@
               <button type="submit" class="btn btn-primary">{{ get_button() }}</button>
               <button id="editor-preview" name="preview" type="submit" class="btn">{% trans %}Preview{% endtrans %}</button>
             </div>
-            {% endif %}
           </form>
 
         </div>
@@ -194,4 +204,14 @@
 
 {% macro get_extra() %}
   <button id="editor-preview" name="preview" type="submit" class="btn pull-right">{% trans %}Preview{% endtrans %}</button>
-{% endmacro %}
+{% endmacro %}
+
+
+{% macro lang_poll_public() -%}
+{% trans %}Make voting in this poll public.{% endtrans %}
+{%- endmacro %}
+
+
+{% macro lang_poll_changing_votes() -%}
+{% trans %}Allow users to change their votes.{% endtrans %}
+{%- endmacro %}

+ 42 - 0
templates/cranefly/threads/thread.html

@@ -39,6 +39,27 @@
   </div>
   {% endif %}
 
+  {% if thread.has_poll %}
+  <div class="thread-poll">
+    <h2>{{ thread.poll.question }}</h2>
+      {% for choice in thread.poll.option_set.all() %}
+      <div style="background: #{{ colorhex(loop.index0) }}">{{ choice.name }}</div>
+      {% endfor %}
+    {% if poll_form %}
+    <form action="" method="post">
+      {{ form_theme.field(poll_form.options) }}
+    </form>
+    {% else %}
+    <ul>
+      {% for choice in thread.poll.option_set.all() %}
+      <li><div style="background: #{{ colorhex(loop.index0) }}">{{ choice.name }}</div></li>
+      {% endfor %}
+    </ul>
+    {% endif %}
+    <div id="thread-poll-graph"></div>
+  </div>
+  {% endif %}
+
   <div class="thread-buttons">
     {{ pager() }}
     {% if user.is_authenticated() %}
@@ -489,6 +510,27 @@
   {% if user.is_authenticated() and acl.threads.can_reply(forum, thread) %}
   {{ editor.js() }}
   {% endif %}
+  {% if thread.has_poll %}
+  <script type="text/javascript" src="https://www.google.com/jsapi"></script>
+  <script type="text/javascript">
+    google.load("visualization", "1", {packages:["corechart"]});
+    google.setOnLoadCallback(drawChart);
+    function drawChart() {
+      var data = google.visualization.arrayToDataTable([
+        ["{{ thread.poll.question }}", "{% trans %}Votes{% endtrans %}"]
+        {% for option in thread.poll.option_set.all() %}
+        , ["{{ option.name }}", {{ option.votes}}]
+        {% endfor %}
+      ]);
+
+      var options = {
+      };
+
+      var chart = new google.visualization.PieChart(document.getElementById('thread-poll-graph'));
+      chart.draw(data, options);
+    }
+  </script>
+  {% endif %}
 {%- endblock %}