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

+ 23 - 2
misago/acl/permissions/threads.py

@@ -568,13 +568,34 @@ class ThreadsACL(BaseACL):
         except KeyError:
             return False
 
-    def can_vote_in_polls(self, forum):
+    def can_vote_in_polls(self, forum, thread, poll):
         try:
             forum_role = self.get_role(forum)
-            return forum_role['can_vote_in_polls']
+            return (forum_role['can_vote_in_polls']
+                    and not forum.closed
+                    and not thread.closed
+                    and not thread.deleted
+                    and not poll.over
+                    and (poll.vote_changing or not poll.user_votes))
         except KeyError:
             return False
 
+    def allow_vote_in_polls(self, forum, thread, poll):
+        try:
+            forum_role = self.get_role(forum)
+            if not forum_role['can_vote_in_polls']:
+                raise ACLError403(_("You don't have permission to vote polls."))
+            if poll.over:
+                raise ACLError403(_("This poll has ended."))
+            if forum.closed or thread.closed:
+                raise ACLError403(_("This poll has been closed."))
+            if thread.deleted:
+                raise ACLError403(_("This poll's thread has been deleted."))
+            if poll.user_votes and not poll.vote_changing:
+                raise ACLError403(_("You have already voted in this poll."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to vote in this poll."))
+
     def can_see_all_checkpoints(self, forum):
         try:
             forum_role = self.get_role(forum)

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

@@ -97,8 +97,8 @@ class PollVoteForm(Form):
 
     def finalize_form(self):
         choices = []
-        for choice in self.poll.option_set.all():
-            choices.append((choice.pk, choice.name))
+        for choice in self.poll.choices_cache:
+            choices.append((choice['pk'], choice['name']))
         if self.poll.max_choices > 1:
             self.add_field('options',
                            forms.TypedMultipleChoiceField(choices=choices, coerce=int,

+ 43 - 1
misago/apps/threads/jumps.py

@@ -1,4 +1,13 @@
+from django.core.urlresolvers import reverse
+from django.db import transaction
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.decorators import check_csrf
+from misago.models import Poll, PollVote
 from misago.apps.threadtype.jumps import *
+from misago.apps.threads.forms import PollVoteForm
 from misago.apps.threads.mixins import TypeMixin
 
 class LastReplyView(LastReplyBaseView, TypeMixin):
@@ -54,4 +63,37 @@ class ReportPostView(ReportPostBaseView, TypeMixin):
 
 
 class ShowPostReportView(ShowPostReportBaseView, TypeMixin):
-    pass
+    pass
+
+
+class VoteInPollView(JumpView, TypeMixin):
+    def check_permissions(self):
+        if self.request.method != 'POST':
+            raise ACLError404()
+        if not self.request.user.is_authenticated():
+            raise ACLError403(_("Only registered users can vote in polls."))
+
+    def make_jump(self):
+        @check_csrf
+        @transaction.commit_on_success
+        def view(request):
+            self.fetch_poll()
+            form = PollVoteForm(self.request.POST, request=self.request, poll=self.poll)
+            if form.is_valid():
+                if self.poll.user_votes:
+                    self.poll.retract_votes(self.poll.user_votes)
+                self.poll.make_vote(self.request, form.cleaned_data['options'])
+                self.poll.save()
+                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')
+        return view(self.request)
+
+    def fetch_poll(self):
+        self.poll = Poll.objects.select_for_update().get(thread=self.thread.pk)
+        if not self.poll:
+            raise ACLError404(_("Poll could not be found."))
+        self.poll.option_set.all()
+        self.poll.user_votes = self.request.user.pollvote_set.filter(poll=self.poll)
+        self.request.acl.threads.allow_vote_in_polls(self.forum, self.thread, self.poll)

+ 3 - 7
misago/apps/threads/thread.py

@@ -10,14 +10,10 @@ class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
         context['poll_form'] = None
         if self.thread.has_poll:
             context['poll'] = self.thread.poll
-            self.thread.poll.option_set.all()
+            self.thread.poll.message = self.request.messages.get_message('poll_%s' % self.thread.poll.pk)
             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)):
+                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):
                     context['poll_form'] = PollVoteForm(poll=self.thread.poll)
         return super(ThreadView, self).template_vars(context)
 

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

@@ -6,6 +6,7 @@ urlpatterns = patterns('misago.apps.threads',
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/start/$', 'posting.NewThreadView', name="thread_start"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="thread_edit"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/vote/$', 'jumps.VoteInPollView', name="thread_poll_vote"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', 'posting.EditReplyView', name="post_edit"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'thread.ThreadView', name="thread"),

+ 37 - 6
misago/models/pollmodel.py

@@ -35,21 +35,52 @@ class Poll(models.Model):
 
     @property
     def choices_cache(self):
-        if self._cache:
+        try:
             return self._cache
+        except AttributeError:
+            pass
 
         try:
             self._cache = pickle.loads(base64.decodestring(self._choices_cache))
         except Exception:
-            self._cache = {}
+            self._cache = []
 
         return self._cache
 
     @choices_cache.setter
     def choices_cache(self, choices):
-        choices_cache = {'order': [], 'choices': {}}
+        choices_cache = []
         for choice in choices:
-            choices_cache['order'].append(choice.pk)
-            choices_cache['choices'][choice.pk] = choice
+            choices_cache.append({
+                'id': choice.pk,
+                'pk': choice.pk,
+                'name': choice.name,
+                'votes': choice.votes
+            })
         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 make_vote(self, request, options):
+        try:
+            len(options)
+        except TypeError:
+            options = (options, )
+
+        for option in self.option_set.all():
+            if option.pk in options:
+                self.votes += 1
+                option.votes += 1
+                option.save()
+                self.vote_set.create(
+                                     forum_id=self.forum_id,
+                                     thread_id=self.thread_id,
+                                     option=option,
+                                     user=request.user,
+                                     date=timezone.now(),
+                                     ip=request.session.get_ip(request),
+                                     agent=request.META.get('HTTP_USER_AGENT'),
+                                     )
+        self.choices_cache = [x for x in self.option_set.all()]

+ 1 - 1
misago/models/pollvotemodel.py

@@ -1,7 +1,7 @@
 from django.db import models
 
 class PollVote(models.Model):
-    poll = models.ForeignKey('Poll')
+    poll = models.ForeignKey('Poll', related_name="vote_set")
     forum = models.ForeignKey('Forum')
     thread = models.ForeignKey('Thread')
     option = models.ForeignKey('PollOption')

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

@@ -1011,8 +1011,12 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{color:#333;text-decoration:n
 .thread-buttons{overflow:auto}.thread-buttons .pull-right{margin-left:14px}
 .thread-buttons .thread-signin-message{float:right}.thread-buttons .thread-signin-message a:link,.thread-buttons .thread-signin-message a:visited{color:#333}
 .thread-poll-body{background-color:#fff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0 0 0 3px #eee;-moz-box-shadow:0 0 0 3px #eee;box-shadow:0 0 0 3px #eee;margin-bottom:20px;padding:10px 20px}.thread-poll-body h2{margin-bottom:20px}
+.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 .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{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 p{margin-left:7px;position:relative;top:6.666666666666667px;color:#999}
 .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}
 .thread-body .post-wrapper .post-body .post-content{background-color:#fff;border:1px solid #e7e7e7;border-radius:5px;margin-left:121px;min-height:100px;position:relative}.thread-body .post-wrapper .post-body .post-content:after,.thread-body .post-wrapper .post-body .post-content:before{right:100%;border:solid transparent;content:"";height:0;width:0;position:absolute;pointer-events:none}
 .thread-body .post-wrapper .post-body .post-content:after{border-color:transparent;border-right-color:#fff;border-width:10.5px;top:14px;margin-top:0}

+ 74 - 38
static/cranefly/css/cranefly/thread.less

@@ -26,42 +26,78 @@
   padding: (@baseLineHeight / 2) @baseLineHeight;
 
   h2 {
-  	margin-bottom: @baseLineHeight;
+    margin-bottom: @baseLineHeight;
+  }
+
+  form {
+    margin: 0px;
+    padding: 0px;
+  }
+
+  .options-form {
+    margin-left: @horizontalComponentOffset;
+
+    label {
+      font-size: @fontSizeLarge;
+    }
   }
 
-	.poll-options {
-		dd {
-			padding-top: 2px;
+  .poll-options {
+    dd {
+      padding-top: 2px;
+
+      .progress {
+        background: none;
+        border: 1px solid darken(@bodyBackground, 15%);
+        border-radius: 10px;
+        .box-shadow(none);
+        height: auto;
+        margin: 0px;
+        padding: 2px;
+
+        .bar {
+          border-radius: 5px;
+          height: 10px;
+
+          min-width: 10px;
+        }
+      }
+
+      .option-details {
+        padding-left: 6px;
 
-		  .progress {
-		  	background: none;
-		  	border: 1px solid darken(@bodyBackground, 15%);
-		  	border-radius: 10px;
-		  	.box-shadow(none);
-		  	height: auto;
-		  	margin: 0px;
-		  	padding: 2px;
+        color: @grayLight;
+        font-size: @fontSizeSmall;
 
-		  	.bar {
-		  		border-radius: 5px;
-		  		height: 10px;
+        strong {
+          padding-right: @fontSizeSmall;
 
-		  		min-width: 10px;
-		  	}
-		  }
+          color: @bluePale;
+          font-weight: normal;
+        }
+      }
+    }
+  }
 
-		  .option-details {
-		  	padding-left: 6px;
+  .poll-footer {
+    overflow: auto;
+    margin-top: @baseLineHeight * -0.5;
+    margin-bottom: @baseLineHeight * 0.5;
+    margin-left: @horizontalComponentOffset;
 
-		  	color: @grayLight;
-		  	font-size: @fontSizeSmall;
-		  }
-		}
-	}
+    .btn, p {
+      float: left;
+      margin-right: @baseFontSize / 2;
+    }
 
-	.poll-footer {
+    p {
+      margin-left: @baseFontSize / 2;
+      position: relative;
+      top: @baseLineHeight / 3;
 
-	}
+      color: @grayLight;
+    }
+  }
 }
 
 // Thread body styles
@@ -468,13 +504,13 @@
         }
 
         i {
-        	margin-right: 2px;
-        	position: relative;
-        	top: 2px;
-        	width: @fontSizeLarge;
+          margin-right: 2px;
+          position: relative;
+          top: 2px;
+          width: @fontSizeLarge;
 
-        	font-size: @fontSizeLarge;
-        	text-align: center;
+          font-size: @fontSizeLarge;
+          text-align: center;
         }
 
         form {
@@ -687,16 +723,16 @@
         left: 2px;
 
         i {
-        	margin: 0px;
+          margin: 0px;
 
-        	color: @grayLight;
+          color: @grayLight;
           font-size: @fontSizeLarge;
         }
 
         &:hover, &:active {
-        	i {
-        	  color: @textColor;
-        	}
+          i {
+            color: @textColor;
+          }
         }
       }
     }

+ 67 - 29
templates/cranefly/threads/thread.html

@@ -39,40 +39,78 @@
   </div>
   {% endif %}
 
-  {% if thread.has_poll %}
+  {% if poll %}
   <div class="thread-poll-body" id="poll">
-    <h2 class="text-center">{{ thread.poll.question }}</h2>
-    {% if poll_form %}
-    <form action="" method="post">
-    {% endif %}
-    <div class="poll-options">
-      <dl class="dl-horizontal">
-        {% for choice in thread.poll.option_set.all() %}
-        <dt>{{ choice.name }}</dt>
-        <dd>
-          <div class="progress">
-            <div class="bar" style="width: {% if poll.votes %}1{% else %}{{ (5 * loop.index0) }}{% endif %}%; background: #{{ color_desaturate(color_spin('049cdb', loop.index0 * 265), loop.length, loop.index/3, 20) }};"></div>
-          </div>
-          <p class="option-details">321 votes, {{ 5 * loop.index0 }}% of all</p>
-        </dd>
-        {% endfor %}
-      </dl>
+    {% if poll.message %}
+    <div class="messages-list">
+      {{ macros.draw_message(poll.message) }}
     </div>
+    {% endif %}
+    <h2 class="text-center">{{ poll.question }}</h2>
+    {% if poll_form %}
+    <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="retreat" value="{{ request_path }}">
+      {% endif %}
+      <div class="poll-options">
+        {% 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>
+        {% else %}
+        <dl class="dl-horizontal">
+          {% for choice in poll.choices_cache %}
+          <dt>{{ choice.name }}</dt>
+          <dd>
+            <div class="progress">
+              <div class="bar" style="width: {% if poll.votes %}{{ (choice.votes * 100 / poll.votes)|int }}{% else %}1{% endif %}%; background: #{{ color_desaturate(color_spin('049cdb', loop.index0 * 265), loop.length, loop.index/3, 20) }};"></div>
+            </div>
+            <p class="option-details">
+              {% if choice.pk in poll.user_votes %}<strong>{% trans %}Your vote{% endtrans %}</strong>{% endif %}
+              {% trans count=choice.votes, votes=choice.votes, percent=(choice.votes * 100 / poll.votes)|int -%}
+              {{ votes }} vote, {{ percent }}% of all
+              {%- pluralize -%}
+              {{ votes }} votes, {{ percent }}% of all
+              {%- endtrans %}
+            </p>
+          </dd>
+          {% endfor %}
+        </dl>
+        {% endif %}
+      </div>
+      <div class="poll-footer">
+        {% 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>
+        {% endif %}
+        {% endif %}
+        {% if poll.public %}
+        <a href="" class="btn">{% trans %}Show voters{% endtrans %}</a>
+        {% endif %}
+        <p>
+          {% if poll.public %}
+          <strong>{% trans %}Voting is public.{% endtrans %}</strong>
+          {% endif %}
+          {% if thread.closed or thread.deleted %}
+          {% trans %}Poll has been closed.{% endtrans %}
+          {% elif thread.poll.over %}
+          {% trans end=poll.end_date|date %}Poll ended on {{ end }}{% endtrans %}
+          {% elif thread.poll.length %}
+          {% trans end=poll.end_date|date %}Poll ends on {{ end }}{% endtrans %}
+          {% endif %}
+        </p>
+      </div>
     {% if poll_form %}
-    {#{{ form_theme.field(poll_form.options) }}#}
     </form>
     {% endif %}
-    <div class="pool-footer">
-      {% if thread.closed or thread.deleted %}
-      <p class="poll-footer">{% trans %}Poll has been closed.{% endtrans %}</p>
-      {% elif thread.poll.over %}
-      <p class="poll-footer">{% trans end=thread.poll.end_date|date %}Poll ended on {{ end }}{% endtrans %}</p>
-      {% elif thread.poll.length %}
-      <p class="poll-footer">{% trans end=thread.poll.end_date|date %}Poll ends on {{ end }}{% endtrans %}</p>
-      {% else %}
-      <p class="poll-footer">{% trans %}Poll by{% endtrans %}</p>
-      {% endif %}
-    </div>
   </div>
   {% endif %}