Browse Source

Voting in polls. #28

Rafał Pitoń 11 years ago
parent
commit
1c20de1f96

+ 2 - 2
misago/apps/threads/details.py

@@ -27,13 +27,13 @@ class PollVotesView(ExtraBaseView, TypeMixin):
         self.request.acl.threads.allow_see_poll_votes(self.forum, self.poll)
         self.request.acl.threads.allow_see_poll_votes(self.forum, self.poll)
 
 
     def response(self):
     def response(self):
-        options = self.poll.option_set.all()
+        options = self.poll.option_set.all().order_by('-votes')
         options_dict = {}
         options_dict = {}
         for option in options:
         for option in options:
             option.votes_list = []
             option.votes_list = []
             options_dict[option.pk] = option
             options_dict[option.pk] = option
 
 
-        for vote in self.poll.vote_set.iterator():
+        for vote in self.poll.vote_set.filter(option__isnull=False).iterator():
             options_dict[vote.option_id].votes_list.append(vote)
             options_dict[vote.option_id].votes_list.append(vote)
 
 
         return render_to_response('threads/poll_votes.html',
         return render_to_response('threads/poll_votes.html',

+ 16 - 3
misago/apps/threads/forms.py

@@ -1,4 +1,4 @@
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext, ugettext_lazy as _
 import floppyforms as forms
 import floppyforms as forms
 from misago.apps.threadtype.posting.forms import NewThreadForm as NewThreadFormBase
 from misago.apps.threadtype.posting.forms import NewThreadForm as NewThreadFormBase
 from misago.forms import Form
 from misago.forms import Form
@@ -101,9 +101,22 @@ class PollVoteForm(Form):
             choices.append((choice['pk'], choice['name']))
             choices.append((choice['pk'], choice['name']))
         if self.poll.max_choices > 1:
         if self.poll.max_choices > 1:
             self.add_field('options',
             self.add_field('options',
-                           forms.TypedMultipleChoiceField(choices=choices, coerce=int,
+                           forms.TypedMultipleChoiceField(choices=choices, coerce=int, required=False,
                                                           widget=forms.CheckboxSelectMultiple))
                                                           widget=forms.CheckboxSelectMultiple))
         else:
         else:
             self.add_field('options',
             self.add_field('options',
-                           forms.TypedChoiceField(choices=choices, coerce=int,
+                           forms.TypedChoiceField(choices=choices, coerce=int, required=False,
                                                   widget=forms.RadioSelect))
                                                   widget=forms.RadioSelect))
+
+    def clean_options(self):
+        data = self.cleaned_data['options']
+        try:
+            if not data:
+                raise forms.ValidationError(_("You have to make selection."))
+            if len(data) > self.poll.max_choices:
+                raise forms.ValidationError(ungettext("You cannot select more than one option.",
+                                                      "You cannot select more than %(limit)s options.",
+                                                      self.poll.max_choices) % {'limit': self.poll.max_choices})
+        except TypeError:
+            pass
+        return data

+ 29 - 3
misago/apps/threads/jumps.py

@@ -78,6 +78,29 @@ class VoteInPollView(JumpView, TypeMixin):
         @transaction.commit_on_success
         @transaction.commit_on_success
         def view(request):
         def view(request):
             self.fetch_poll()
             self.fetch_poll()
+
+            if 'show_results' in request.POST:
+                if not self.poll.user_votes:
+                    self.poll.make_empty_vote(request)
+                    self.poll.save()
+                return self.poll_redirect()
+
+            if 'show_options' in request.POST:
+                if self.poll.user_votes and not self.poll.user_votes[0].option_id:
+                    self.poll.retract_votes(self.poll.user_votes)
+                    self.poll.save()
+                return self.poll_redirect()
+
+            if 'change_vote' in request.POST:
+                if self.poll.user_votes and self.poll.user_votes[0].option_id and self.poll.vote_changing:
+                    self.poll.retract_votes(self.poll.user_votes)
+                    self.poll.save()
+                return self.poll_redirect()
+
+            if self.poll.user_votes and self.poll.user_votes[0].option_id and not self.poll.vote_changing:
+                messages.error(_("Changing vote in this poll is not allowed."), 'poll_%s' % self.poll.pk)
+                return self.poll_redirect()
+
             form = PollVoteForm(self.request.POST, request=self.request, poll=self.poll)
             form = PollVoteForm(self.request.POST, request=self.request, poll=self.poll)
             if form.is_valid():
             if form.is_valid():
                 if self.poll.user_votes:
                 if self.poll.user_votes:
@@ -85,11 +108,14 @@ class VoteInPollView(JumpView, TypeMixin):
                 self.poll.make_vote(self.request, form.cleaned_data['options'])
                 self.poll.make_vote(self.request, form.cleaned_data['options'])
                 self.poll.save()
                 self.poll.save()
                 messages.success(self.request, _("Your vote has been cast."), 'poll_%s' % self.poll.pk)
                 messages.success(self.request, _("Your vote has been cast."), 'poll_%s' % self.poll.pk)
-            else:
-                messages.error(self.request, form.errors['__all__'][0], 'poll_%s' % self.poll.pk)
-            return redirect(self.request.POST.get('retreat', reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug})) + '#poll')
+            elif 'options' in form.errors:
+                messages.error(self.request, form.errors['options'][0], 'poll_%s' % self.poll.pk)
+            return self.poll_redirect()
         return view(self.request)
         return view(self.request)
 
 
+    def poll_redirect(self):
+        return redirect(self.request.POST.get('retreat', reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug})) + '#poll')
+
     def fetch_poll(self):
     def fetch_poll(self):
         self.poll = Poll.objects.select_for_update().get(thread=self.thread.pk)
         self.poll = Poll.objects.select_for_update().get(thread=self.thread.pk)
         if not self.poll:
         if not self.poll:

+ 2 - 2
misago/apps/threads/posting.py

@@ -18,8 +18,8 @@ class NewThreadView(NewThreadBaseView, TypeMixin):
         poll = Poll(forum=self.forum,
         poll = Poll(forum=self.forum,
                     thread=self.thread,
                     thread=self.thread,
                     user=self.request.user,
                     user=self.request.user,
-                    user_name=self.request.username,
-                    user_slug=self.request.user_slug,
+                    user_name=self.request.user.username,
+                    user_slug=self.request.user.username_slug,
                     start_date=timezone.now(),
                     start_date=timezone.now(),
                     length=form.cleaned_data['poll_length'],
                     length=form.cleaned_data['poll_length'],
                     question=form.cleaned_data['poll_question'],
                     question=form.cleaned_data['poll_question'],

+ 4 - 1
misago/apps/threads/thread.py

@@ -6,6 +6,10 @@ from misago.apps.threads.mixins import TypeMixin
 
 
 class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
 class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
     def template_vars(self, context):
     def template_vars(self, context):
+        self.add_poll(context)
+        return super(ThreadView, self).template_vars(context)
+
+    def add_poll(self, context):
         context['poll'] = None
         context['poll'] = None
         context['poll_form'] = None
         context['poll_form'] = None
         if self.thread.has_poll:
         if self.thread.has_poll:
@@ -15,7 +19,6 @@ class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
                 self.thread.poll.user_votes = [x.option_id for x in self.request.user.pollvote_set.filter(poll=self.thread.poll)]
                 self.thread.poll.user_votes = [x.option_id for x in self.request.user.pollvote_set.filter(poll=self.thread.poll)]
                 if self.request.acl.threads.can_vote_in_polls(self.forum, self.thread, self.thread.poll):
                 if self.request.acl.threads.can_vote_in_polls(self.forum, self.thread, self.thread.poll):
                     context['poll_form'] = PollVoteForm(poll=self.thread.poll)
                     context['poll_form'] = PollVoteForm(poll=self.thread.poll)
-        return super(ThreadView, self).template_vars(context)
 
 
     def posts_actions(self):
     def posts_actions(self):
         acl = self.request.acl.threads.get_role(self.thread.forum_id)
         acl = self.request.acl.threads.get_role(self.thread.forum_id)

+ 437 - 0
misago/migrations/0025_auto__chg_field_pollvote_option.py

@@ -0,0 +1,437 @@
+# -*- 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):
+
+        # Changing field 'PollVote.option'
+        db.alter_column(u'misago_pollvote', 'option_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.PollOption'], null=True))
+
+    def backwards(self, orm):
+
+        # User chose to not deal with backwards NULL issues for 'PollVote.option'
+        raise RuntimeError("Cannot reverse this migration. 'PollVote.option' and its values cannot be restored.")
+
+    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'}),
+            '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.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']

+ 33 - 5
misago/models/pollmodel.py

@@ -35,6 +35,9 @@ class Poll(models.Model):
             return False
             return False
         return timezone.now() > self.end_date
         return timezone.now() > self.end_date
 
 
+    def make_choices_cache(self):
+        self.choices_cache = [x for x in self.option_set.all()]
+
     @property
     @property
     def choices_cache(self):
     def choices_cache(self):
         try:
         try:
@@ -62,10 +65,23 @@ class Poll(models.Model):
         self._cache = choices_cache
         self._cache = choices_cache
         self._choices_cache = base64.encodestring(pickle.dumps(choices_cache, pickle.HIGHEST_PROTOCOL))
         self._choices_cache = base64.encodestring(pickle.dumps(choices_cache, pickle.HIGHEST_PROTOCOL))
 
 
-    def retract_vote(self, votes):
-        pass
+    def retract_votes(self, votes):
+        options = self.option_set.all()
+        options_dict = {}
+        for option in options:
+            options_dict[option.pk] = option
+
+        for vote in votes:
+            if vote.option_id in options_dict:
+                self.votes -= 1
+                options_dict[vote.option_id].votes -= 1
+        self.vote_set.filter(id__in=[x.pk for x in votes]).delete()
 
 
-    def make_vote(self, request, options):
+        for option in options:
+            option.save()
+        self.choices_cache = options
+
+    def make_vote(self, request, options=None):
         try:
         try:
             len(options)
             len(options)
         except TypeError:
         except TypeError:
@@ -82,9 +98,21 @@ class Poll(models.Model):
                                      option=option,
                                      option=option,
                                      user=request.user,
                                      user=request.user,
                                      user_name=request.user.username,
                                      user_name=request.user.username,
-                                     user_slug=request.user.slug,
+                                     user_slug=request.user.username_slug,
                                      date=timezone.now(),
                                      date=timezone.now(),
                                      ip=request.session.get_ip(request),
                                      ip=request.session.get_ip(request),
                                      agent=request.META.get('HTTP_USER_AGENT'),
                                      agent=request.META.get('HTTP_USER_AGENT'),
                                      )
                                      )
-        self.choices_cache = [x for x in self.option_set.all()]
+        self.make_choices_cache()
+
+    def make_empty_vote(self, request):
+        self.vote_set.create(
+                             forum_id=self.forum_id,
+                             thread_id=self.thread_id,
+                             user=request.user,
+                             user_name=request.user.username,
+                             user_slug=request.user.username_slug,
+                             date=timezone.now(),
+                             ip=request.session.get_ip(request),
+                             agent=request.META.get('HTTP_USER_AGENT'),
+                             )

+ 1 - 1
misago/models/pollvotemodel.py

@@ -4,7 +4,7 @@ class PollVote(models.Model):
     poll = models.ForeignKey('Poll', related_name="vote_set")
     poll = models.ForeignKey('Poll', related_name="vote_set")
     forum = models.ForeignKey('Forum')
     forum = models.ForeignKey('Forum')
     thread = models.ForeignKey('Thread')
     thread = models.ForeignKey('Thread')
-    option = models.ForeignKey('PollOption')
+    option = models.ForeignKey('PollOption', null=True, blank=True)
     user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
     user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
     user_name = models.CharField(max_length=255, null=True, blank=True)
     user_name = models.CharField(max_length=255, null=True, blank=True)
     user_slug = models.SlugField(max_length=255, null=True, blank=True)
     user_slug = models.SlugField(max_length=255, null=True, blank=True)

+ 3 - 2
static/cranefly/css/cranefly.css

@@ -1010,9 +1010,10 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{color:#333;text-decoration:n
 .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}
 .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-buttons{overflow:auto}.thread-buttons .pull-right{margin-left:14px}
 .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-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-bottom:20px}
+.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}
 .thread-poll-body form{margin:0;padding:0}
 .thread-poll-body form{margin:0;padding:0}
-.thread-poll-body .options-form{margin-left:180px}.thread-poll-body .options-form label{font-size:17.5px}
+.thread-poll-body .options-form{margin-left:180px;margin-bottom:20px}.thread-poll-body .options-form label{padding:2px 0;font-size:17.5px}
+.thread-poll-body .options-form .poll-voting-help{color:#999}
 .thread-poll-body .poll-options dd{padding-top:2px}.thread-poll-body .poll-options dd .progress{background:none;border:1px solid #d5d5d5;border-radius:10px;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;height:auto;margin:0;padding:2px}.thread-poll-body .poll-options dd .progress .bar{border-radius:5px;height:10px;min-width:10px}
 .thread-poll-body .poll-options dd{padding-top:2px}.thread-poll-body .poll-options dd .progress{background:none;border:1px solid #d5d5d5;border-radius:10px;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;height:auto;margin:0;padding:2px}.thread-poll-body .poll-options dd .progress .bar{border-radius:5px;height:10px;min-width:10px}
 .thread-poll-body .poll-options dd .option-details{padding-left:6px;color:#999;font-size:11.9px}.thread-poll-body .poll-options dd .option-details strong{padding-right:11.9px;color:#3c85a3;font-weight:normal}
 .thread-poll-body .poll-options dd .option-details{padding-left:6px;color:#999;font-size:11.9px}.thread-poll-body .poll-options dd .option-details strong{padding-right:11.9px;color:#3c85a3;font-weight:normal}
 .thread-poll-body .poll-footer{overflow:auto;margin-top:-10px;margin-bottom:10px;margin-left:180px}.thread-poll-body .poll-footer .btn,.thread-poll-body .poll-footer p{float:left;margin-right:7px}
 .thread-poll-body .poll-footer{overflow:auto;margin-top:-10px;margin-bottom:10px;margin-left:180px}.thread-poll-body .poll-footer .btn,.thread-poll-body .poll-footer p{float:left;margin-right:7px}

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

@@ -26,6 +26,7 @@
   padding: (@baseLineHeight / 2) @baseLineHeight;
   padding: (@baseLineHeight / 2) @baseLineHeight;
 
 
   h2 {
   h2 {
+    margin-left: @horizontalComponentOffset;
     margin-bottom: @baseLineHeight;
     margin-bottom: @baseLineHeight;
   }
   }
 
 
@@ -36,10 +37,17 @@
 
 
   .options-form {
   .options-form {
     margin-left: @horizontalComponentOffset;
     margin-left: @horizontalComponentOffset;
+    margin-bottom: @baseLineHeight;
 
 
     label {
     label {
+    	padding: 2px 0px;
+
       font-size: @fontSizeLarge;
       font-size: @fontSizeLarge;
     }
     }
+
+    .poll-voting-help {
+      color: @grayLight;
+    }
   }
   }
 
 
   .poll-options {
   .poll-options {

+ 18 - 17
templates/cranefly/threads/thread.html

@@ -46,7 +46,7 @@
       {{ macros.draw_message(poll.message) }}
       {{ macros.draw_message(poll.message) }}
     </div>
     </div>
     {% endif %}
     {% endif %}
-    <h2 class="text-center">{{ poll.question }}</h2>
+    <h2>{% trans question=poll.question %}Poll: {{ question }}{% endtrans %}</h2>
     {% if poll_form %}
     {% if poll_form %}
     <form action="{{ url('thread_poll_vote', slug=thread.slug, thread=thread.pk) }}" method="post">
     <form action="{{ url('thread_poll_vote', slug=thread.slug, thread=thread.pk) }}" method="post">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
@@ -54,16 +54,14 @@
       {% endif %}
       {% endif %}
       <div class="poll-options">
       <div class="poll-options">
         {% if poll_form and not poll.user_votes %}
         {% if poll_form and not poll.user_votes %}
-        <ul class="unstyled options-form">
-          {% for choice in poll.choices_cache %}
-          <li>
-            <label class="radio">
-              <input type="radio" name="options" id="id_options_{{ choice.pk }}" value="{{ choice.pk }}">
-              {{ choice.name }}
-            </label>
-          </li>
-          {% endfor %}
-        </ul>
+        <div class="options-form">
+          {{ form_theme.field(poll_form.options) }}
+          <p class="poll-voting-help">{% trans choices=poll.max_choices -%}
+            You can vote on one option.
+          {%- pluralize -%}
+            You can vote on {{ choices }} options.
+          {%- endtrans %}{% if poll.public %} {% trans %}Your votes will be public.{% endtrans %}{% endif %}</p>
+        </div>
         {% else %}
         {% else %}
         <dl class="dl-horizontal">
         <dl class="dl-horizontal">
           {% for choice in poll.choices_cache %}
           {% for choice in poll.choices_cache %}
@@ -87,18 +85,21 @@
       </div>
       </div>
       <div class="poll-footer">
       <div class="poll-footer">
         {% if poll_form %}
         {% if poll_form %}
-        <button type="submit" class="btn btn-primary">{% trans %}Vote{% endtrans %}</button>
-        {% if not poll.user_votes %}
-        <button type="submit" name="empty_vote" class="btn btn-inverse">{% trans %}Results{% endtrans %}</button>
+        {% if poll.user_votes and (not poll.user_votes[0] or poll.vote_changing) %}
+        {% if poll.user_votes[0] %}
+        <button type="submit" name="change_vote" class="btn btn-primary">{% trans %}Change vote{% endtrans %}</button>
+        {% else %}
+        <button type="submit" name="show_options" class="btn btn-primary">{% trans %}Voting Options{% endtrans %}</button>
+        {% endif %}
+        {% else %}
+        <button type="submit" class="btn btn-primary">{% trans %}Make Vote{% endtrans %}</button>
+        <button type="submit" name="show_results" class="btn btn-inverse">{% trans %}Results{% endtrans %}</button>
         {% endif %}
         {% endif %}
         {% endif %}
         {% endif %}
         {% if poll.public or acl.threads.can_see_polls_votes(forum) %}
         {% if poll.public or acl.threads.can_see_polls_votes(forum) %}
         <a href="{{ url('thread_poll_details', slug=thread.slug, thread=thread.pk) }}" class="btn">{% trans %}Show voters{% endtrans %}</a>
         <a href="{{ url('thread_poll_details', slug=thread.slug, thread=thread.pk) }}" class="btn">{% trans %}Show voters{% endtrans %}</a>
         {% endif %}
         {% endif %}
         <p>
         <p>
-          {% if poll.public %}
-          <strong>{% trans %}Voting is public.{% endtrans %}</strong>
-          {% endif %}
           {% if thread.closed or thread.deleted %}
           {% if thread.closed or thread.deleted %}
           {% trans %}Poll has been closed.{% endtrans %}
           {% trans %}Poll has been closed.{% endtrans %}
           {% elif thread.poll.over %}
           {% elif thread.poll.over %}