diff --git a/CHANGELOG b/CHANGELOG index 6c8b0a52b..e387a527a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,7 @@ Participants: Files: - Enabled update and delete view for uploader refering to his own files. Other: +- New config option to set the 100% base for polls (motions/elections). - Changed widget api. Used new metaclass. - Changed api for plugins. Used entry points to detect them automaticly. - Renamed config api classes. diff --git a/openslides/assignment/models.py b/openslides/assignment/models.py index 468d8c991..3f1485ff9 100644 --- a/openslides/assignment/models.py +++ b/openslides/assignment/models.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.agenda.models import Item, Speaker from openslides.config.api import config from openslides.poll.models import (BaseOption, BasePoll, BaseVote, - CollectInvalid, CollectVotesCast, + CollectDefaultVotesMixin, PublishPollMixin) from openslides.projector.models import RelatedModelMixin, SlideMixin from openslides.utils.exceptions import OpenSlidesError @@ -271,7 +271,7 @@ class AssignmentOption(BaseOption): return unicode(self.candidate) -class AssignmentPoll(RelatedModelMixin, CollectInvalid, CollectVotesCast, +class AssignmentPoll(RelatedModelMixin, CollectDefaultVotesMixin, PublishPollMixin, AbsoluteUrlMixin, BasePoll): option_class = AssignmentOption assignment = models.ForeignKey(Assignment, related_name='poll_set') @@ -319,5 +319,9 @@ class AssignmentPoll(RelatedModelMixin, CollectInvalid, CollectVotesCast, def get_ballot(self): return self.assignment.poll_set.filter(id__lte=self.id).count() + def get_percent_base_choice(self): + return config['assignment_poll_100_percent_base'] + def append_pollform_fields(self, fields): fields.append('description') + super(AssignmentPoll, self).append_pollform_fields(fields) diff --git a/openslides/assignment/signals.py b/openslides/assignment/signals.py index 780969c1e..642eab5f3 100644 --- a/openslides/assignment/signals.py +++ b/openslides/assignment/signals.py @@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable from openslides.config.signals import config_signal +from openslides.poll.models import PERCENT_BASE_CHOICES @receiver(config_signal, dispatch_uid='setup_assignment_config') @@ -26,6 +27,14 @@ def setup_assignment_config(sender, **kwargs): ('auto', ugettext_lazy('Automatic assign of method')), ('votes', ugettext_lazy('Always one option per candidate')), ('yesnoabstain', ugettext_lazy('Always Yes-No-Abstain per candidate'))))) + assignment_poll_100_percent_base = ConfigVariable( + name='assignment_poll_100_percent_base', + default_value='WITHOUT_INVALID', + form_field=forms.ChoiceField( + widget=forms.Select(), + required=False, + label=ugettext_lazy('The 100 % base of an election result consists of'), + choices=PERCENT_BASE_CHOICES)) assignment_pdf_ballot_papers_selection = ConfigVariable( name='assignment_pdf_ballot_papers_selection', default_value='CUSTOM_NUMBER', @@ -55,6 +64,7 @@ def setup_assignment_config(sender, **kwargs): group_ballot = ConfigGroup( title=ugettext_lazy('Ballot and ballot papers'), variables=(assignment_poll_vote_values, + assignment_poll_100_percent_base, assignment_pdf_ballot_papers_selection, assignment_pdf_ballot_papers_number, assignment_publish_winner_results_only)) diff --git a/openslides/assignment/templates/assignment/assignment_detail.html b/openslides/assignment/templates/assignment/assignment_detail.html index 2407e2e0d..f833af89b 100644 --- a/openslides/assignment/templates/assignment/assignment_detail.html +++ b/openslides/assignment/templates/assignment/assignment_detail.html @@ -187,8 +187,8 @@ {% if candidate in assignment.elected %} {% if perms.assignment.can_manage_assignment %} - + {% else %} @@ -196,7 +196,8 @@ {% endif %} {% else %} {% if perms.assignment.can_manage_assignment %} - + {% endif %} {% endif %} {{ candidate }} @@ -221,13 +222,29 @@ {% endif %} {% endfor %} + + {% trans 'Valid votes' %} + {% for poll in polls %} + {% if poll.published or perms.assignment.can_manage_assignment %} + + {% if poll.has_votes %} + + {{ poll.print_votesvalid }} + {% endif %} + + {% endif %} + {% endfor %} + {% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %} + + {% endif %} + {% trans 'Invalid votes' %} {% for poll in polls %} {% if poll.published or perms.assignment.can_manage_assignment %} {% if poll.has_votes %} - + {{ poll.print_votesinvalid }} {% endif %} @@ -238,13 +255,13 @@ {% endif %} - {% trans 'Votes cast' %} + {% trans 'Votes cast' %} {% for poll in polls %} {% if poll.published or perms.assignment.can_manage_assignment %} {% if poll.has_votes %} - {{ poll.print_votescast }} + {{ poll.print_votescast }} {% endif %} {% endif %} diff --git a/openslides/assignment/templates/assignment/poll_view.html b/openslides/assignment/templates/assignment/poll_view.html index 40e440c31..961397612 100644 --- a/openslides/assignment/templates/assignment/poll_view.html +++ b/openslides/assignment/templates/assignment/poll_view.html @@ -50,6 +50,16 @@ {% endfor %} {% endfor %} + + {% trans "Valid votes" %} + {% for value in poll.get_vote_values %} + {% if forloop.first %} + {{ pollform.votesvalid.errors }}{{ pollform.votesvalid }} + {% else %} + + {% endif %} + {% endfor %} + {% trans "Invalid votes" %} {% for value in poll.get_vote_values %} diff --git a/openslides/assignment/templates/assignment/slide.html b/openslides/assignment/templates/assignment/slide.html index 51de3ce2c..aec360c7e 100644 --- a/openslides/assignment/templates/assignment/slide.html +++ b/openslides/assignment/templates/assignment/slide.html @@ -80,12 +80,23 @@ {% endfor %} {% endfor %} + + {% trans 'Valid votes' %} + {% for poll in polls %} + + {% if poll.has_votes %} + + {{ poll.print_votesvalid }} + {% endif %} + + {% endfor %} + {% trans 'Invalid votes' %} {% for poll in polls %} - {% if poll.has_votes %} - + {% if poll.has_votes %} + {{ poll.print_votesinvalid }} {% endif %} @@ -94,13 +105,13 @@ - {% trans 'Votes cast' %} + {% trans 'Votes cast' %} {% for poll in polls %} {% if poll.has_votes %} - {{ poll.print_votescast }} + {{ poll.print_votescast }} {% endif %} {% endfor %} diff --git a/openslides/assignment/views.py b/openslides/assignment/views.py index e1ba14f5b..5a4e0838a 100644 --- a/openslides/assignment/views.py +++ b/openslides/assignment/views.py @@ -423,19 +423,29 @@ class AssignmentPDF(PDFView): pass data_votes.append(row) - # Add votes invalid row - footrow_one = [] - footrow_one.append(_("Invalid votes")) - for poll in polls: - footrow_one.append(poll.print_votesinvalid()) - data_votes.append(footrow_one) + # Add valid votes row + if poll.votesvalid is not None: + footrow_one = [] + footrow_one.append(_("Valid votes")) + for poll in polls: + footrow_one.append(poll.print_votesvalid()) + data_votes.append(footrow_one) + + # Add invalid votes row + if poll.votesinvalid is not None: + footrow_two = [] + footrow_two.append(_("Invalid votes")) + for poll in polls: + footrow_two.append(poll.print_votesinvalid()) + data_votes.append(footrow_two) # Add votes cast row - footrow_two = [] - footrow_two.append(_("Votes cast")) - for poll in polls: - footrow_two.append(poll.print_votescast()) - data_votes.append(footrow_two) + if poll.votescast is not None: + footrow_three = [] + footrow_three.append(_("Votes cast")) + for poll in polls: + footrow_three.append(poll.print_votescast()) + data_votes.append(footrow_three) table_votes = Table(data_votes) table_votes.setStyle( diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 7acbce461..912abe31b 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -9,8 +9,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.config.api import config from openslides.mediafile.models import Mediafile -from openslides.poll.models import (BaseOption, BasePoll, BaseVote, - CollectInvalid, CollectVotesCast) +from openslides.poll.models import (BaseOption, BasePoll, BaseVote, CollectDefaultVotesMixin) from openslides.projector.models import RelatedModelMixin, SlideMixin from jsonfield import JSONField from openslides.utils.models import AbsoluteUrlMixin @@ -694,8 +693,7 @@ class MotionOption(BaseOption): """The VoteClass, to witch this Class links.""" -class MotionPoll(RelatedModelMixin, CollectInvalid, CollectVotesCast, - AbsoluteUrlMixin, BasePoll): +class MotionPoll(RelatedModelMixin, CollectDefaultVotesMixin, AbsoluteUrlMixin, BasePoll): """The Class to saves the poll results for a motion poll.""" motion = models.ForeignKey(Motion, related_name='polls') @@ -746,6 +744,9 @@ class MotionPoll(RelatedModelMixin, CollectInvalid, CollectVotesCast, def get_related_model(self): return self.motion + def get_percent_base_choice(self): + return config['motion_poll_100_percent_base'] + class State(models.Model): """ diff --git a/openslides/motion/pdf.py b/openslides/motion/pdf.py index 6257192c5..fe41af937 100644 --- a/openslides/motion/pdf.py +++ b/openslides/motion/pdf.py @@ -113,17 +113,21 @@ def motion_to_pdf(pdf, motion): for poll in polls: ballotcounter += 1 option = poll.get_options()[0] - yes, no, abstain, invalid, votecast = ( - option['Yes'], option['No'], option['Abstain'], - poll.print_votesinvalid(), poll.print_votescast()) - + yes, no, abstain = (option['Yes'], option['No'], option['Abstain']) + valid, invalid, votescast = ('', '', '') + if poll.votesvalid is not None: + valid = "
%s: %s" % (_("Valid votes"), poll.print_votesvalid()) + if poll.votesinvalid is not None: + invalid = "
%s: %s" % (_("Invalid votes"), poll.print_votesinvalid()) + if poll.votescast is not None: + votescast = "
%s: %s" % (_("Votes cast"), poll.print_votescast()) if len(polls) > 1: cell6b.append(Paragraph("%s. %s" % (ballotcounter, _("Vote")), stylesheet['Bold'])) cell6b.append(Paragraph( - "%s: %s
%s: %s
%s: %s
%s: %s
%s: %s" % - (_("Yes"), yes, _("No"), no, _("Abstention"), abstain, _("Invalid"), - invalid, _("Votes cast"), votecast), stylesheet['Normal'])) + "%s: %s
%s: %s
%s: %s
%s %s %s" % + (_("Yes"), yes, _("No"), no, _("Abstention"), abstain, valid, invalid, votescast), + stylesheet['Normal'])) cell6b.append(Spacer(0, 0.2 * cm)) motion_data.append([cell6a, cell6b]) diff --git a/openslides/motion/signals.py b/openslides/motion/signals.py index 28acaf9c1..99175398d 100644 --- a/openslides/motion/signals.py +++ b/openslides/motion/signals.py @@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable from openslides.config.signals import config_signal from openslides.core.signals import post_database_setup +from openslides.poll.models import PERCENT_BASE_CHOICES from .models import State, Workflow @@ -85,7 +86,15 @@ def setup_motion_config(sender, **kwargs): title=ugettext_lazy('Supporters'), variables=(motion_min_supporters, motion_remove_supporters)) - # Ballot papers + # Voting and ballot papers + motion_poll_100_percent_base = ConfigVariable( + name='motion_poll_100_percent_base', + default_value='WITHOUT_INVALID', + form_field=forms.ChoiceField( + widget=forms.Select(), + required=False, + label=ugettext_lazy('The 100 % base of a voting result consists of'), + choices=PERCENT_BASE_CHOICES)) motion_pdf_ballot_papers_selection = ConfigVariable( name='motion_pdf_ballot_papers_selection', default_value='CUSTOM_NUMBER', @@ -106,8 +115,8 @@ def setup_motion_config(sender, **kwargs): min_value=1, label=ugettext_lazy('Custom number of ballot papers'))) group_ballot_papers = ConfigGroup( - title=ugettext_lazy('Ballot papers'), - variables=(motion_pdf_ballot_papers_selection, motion_pdf_ballot_papers_number)) + title=ugettext_lazy('Voting and ballot papers'), + variables=(motion_poll_100_percent_base, motion_pdf_ballot_papers_selection, motion_pdf_ballot_papers_number)) # PDF motion_pdf_title = ConfigVariable( diff --git a/openslides/motion/static/css/motion.css b/openslides/motion/static/css/motion.css index 672e7c5b4..6beb5e98a 100644 --- a/openslides/motion/static/css/motion.css +++ b/openslides/motion/static/css/motion.css @@ -50,3 +50,9 @@ td.diff_header { #motion-vote-results img { margin-top: -4px; } +#motion-vote-results .resultline { + border-top: 1px solid; + padding-top: 5px; + margin: 5px 0; + width: 10em; +} diff --git a/openslides/motion/templates/motion/motion_detail.html b/openslides/motion/templates/motion/motion_detail.html index 3682b5df2..ec24a1cd3 100644 --- a/openslides/motion/templates/motion/motion_detail.html +++ b/openslides/motion/templates/motion/motion_detail.html @@ -221,10 +221,21 @@ {{ option.Yes }}
{{ option.No }}
{{ option.Abstain }}
- {{ poll.print_votesinvalid }}
-
- {{ poll.print_votescast }} -
+ {% if poll.votesvalid != None or poll.votesinvalid != None %} +
+ {% if poll.votesvalid != None %} + {{ poll.print_votesvalid }}
+ {% endif %} + {% if poll.votesinvalid != None %} + {{ poll.print_votesinvalid }}
+ {% endif %} +
+ {% endif %} + {% if poll.votescast != None %} +
+ {{ poll.print_votescast }} +
+ {% endif %} {% endwith %} {% else %} {% if perms.motion.can_manage_motion %} diff --git a/openslides/motion/templates/motion/poll_form.html b/openslides/motion/templates/motion/poll_form.html index 9e7fb5930..adc2fa280 100644 --- a/openslides/motion/templates/motion/poll_form.html +++ b/openslides/motion/templates/motion/poll_form.html @@ -45,6 +45,10 @@ {% endfor %} + {% trans "Valid votes" %} + {{ pollform.votesvalid.errors }}{{ pollform.votesvalid }} + + {% trans "Invalid votes" %} {{ pollform.votesinvalid.errors }}{{ pollform.votesinvalid }} diff --git a/openslides/motion/templates/motion/slide.html b/openslides/motion/templates/motion/slide.html index 2bc475397..e4f3c4ed2 100644 --- a/openslides/motion/templates/motion/slide.html +++ b/openslides/motion/templates/motion/slide.html @@ -23,9 +23,19 @@ {{ option.Yes }}
{{ option.No }}
{{ option.Abstain }}
- {{ poll.print_votesinvalid }}
-
- {{ poll.print_votescast }} + {% if poll.votesvalid != None or poll.votesinvalid != None %} +
+ {% if poll.votesvalid != None %} + {{ poll.print_votesvalid }}
+ {% endif %} + {% if poll.votesinvalid != None %} + {{ poll.print_votesinvalid }}
+ {% endif %} + {% endif %} + {% if poll.votescast != None %} +
+ {{ poll.print_votescast }} + {% endif %} {% endwith %} {% else %} diff --git a/openslides/poll/models.py b/openslides/poll/models.py index d4450e781..b5f8014d3 100644 --- a/openslides/poll/models.py +++ b/openslides/poll/models.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import locale + from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils.translation import ugettext as _ @@ -60,17 +62,28 @@ class BaseVote(models.Model): if raw: return self.weight try: - percent_base = self.option.poll.percent_base() + percent_base = self.option.poll.get_percent_base() except AttributeError: # The poll class is no child of CollectVotesCast percent_base = 0 return print_value(self.weight, percent_base) -class CollectVotesCast(models.Model): +PERCENT_BASE_CHOICES = ( + ('WITHOUT_INVALID', ugettext_lazy('All valid votes (Yes + No + Abstention)')), + ('WITH_INVALID', ugettext_lazy('All votes casts (valid + invalid votes)')), + ('DISABLED', ugettext_lazy('Disabled (no percents)'))) + + +class CollectDefaultVotesMixin(models.Model): """ - Mixin for a poll to collect the votes cast. + Mixin for a poll to collect the default vote values for valid votes, + invalid votes and votes cast. """ + votesvalid = MinMaxIntegerField(null=True, blank=True, min_value=-2, + verbose_name=ugettext_lazy('Votes valid')) + votesinvalid = MinMaxIntegerField(null=True, blank=True, min_value=-2, + verbose_name=ugettext_lazy('Votes invalid')) votescast = MinMaxIntegerField(null=True, blank=True, min_value=-2, verbose_name=ugettext_lazy('Votes cast')) @@ -78,39 +91,46 @@ class CollectVotesCast(models.Model): abstract = True def append_pollform_fields(self, fields): - fields.append('votescast') - super(CollectVotesCast, self).append_pollform_fields(fields) - - def print_votescast(self): - return print_value(self.votescast, self.percent_base()) - - def percent_base(self): - if self.votescast > 0: - return 100 / float(self.votescast) - return 0 - - -class CollectInvalid(models.Model): - """ - Mixin for a poll to collect invalid votes. - """ - votesinvalid = MinMaxIntegerField(null=True, blank=True, min_value=-2, - verbose_name=ugettext_lazy('Votes invalid')) - - class Meta: - abstract = True - - def append_pollform_fields(self, fields): + fields.append('votesvalid') fields.append('votesinvalid') - super(CollectInvalid, self).append_pollform_fields(fields) + fields.append('votescast') + super(CollectDefaultVotesMixin, self).append_pollform_fields(fields) + + def get_percent_base_choice(self): + """ + Returns one of the three strings in PERCENT_BASE_CHOICES. + """ + raise NotImplementedError('You have to provide a get_percent_base_choice() method.') + + def print_votesvalid(self): + if self.get_percent_base_choice() == 'DISABLED': + value = print_value(self.votesvalid, None) + else: + value = print_value(self.votesvalid, self.get_percent_base()) + return value def print_votesinvalid(self): - try: - percent_base = self.percent_base() - except AttributeError: - # The poll class is no child of CollectVotesCast - percent_base = 0 - return print_value(self.votesinvalid, percent_base) + if self.get_percent_base_choice() == 'WITH_INVALID': + value = print_value(self.votesinvalid, self.get_percent_base()) + else: + value = print_value(self.votesinvalid, None) + return value + + def print_votescast(self): + if self.get_percent_base_choice() == 'WITH_INVALID': + value = print_value(self.votescast, self.get_percent_base()) + else: + value = print_value(self.votescast, None) + return value + + def get_percent_base(self): + if self.get_percent_base_choice() == "WITHOUT_INVALID" and self.votesvalid > 0: + base = 100 / float(self.votesvalid) + elif self.get_percent_base_choice() == "WITH_INVALID" and self.votescast > 0: + base = 100 / float(self.votescast) + else: + base = None + return base class PublishPollMixin(models.Model): @@ -251,7 +271,8 @@ def print_value(value, percent_base=0): verbose_value = _('undocumented') else: if percent_base: - verbose_value = u'%d (%.2f %%)' % (value, value * percent_base) + locale.setlocale(locale.LC_ALL, '') + verbose_value = u'%d (%s %%)' % (value, locale.format('%.1f', value * percent_base)) else: verbose_value = u'%s' % value return verbose_value diff --git a/openslides/projector/static/css/projector.css b/openslides/projector/static/css/projector.css index f6b52833a..b2370afc0 100644 --- a/openslides/projector/static/css/projector.css +++ b/openslides/projector/static/css/projector.css @@ -104,9 +104,10 @@ li { .well h4.first { margin-top: 0; } -.well .results hr { +.resultline { + border-top: 1px solid; margin: 5px 0; - border: 1px solid #E3E3E3; + width: 10em; } hr { margin: 10px 0; diff --git a/tests/motion/test_views.py b/tests/motion/test_views.py index 4b9b1de3c..7cbe55934 100644 --- a/tests/motion/test_views.py +++ b/tests/motion/test_views.py @@ -86,10 +86,10 @@ class TestMotionDetailView(MotionViewTestCase): response = self.staff_client.post( '/motion/1/poll/1/edit/', {'option-1-Yes': '10', - 'pollform-votescast': '50'}) + 'pollform-votesvalid': '50'}) self.assertRedirects(response, '/motion/1/') response = self.staff_client.get('/motion/1/') - self.assertContains(response, '100.00 %') + self.assertContains(response, '50 (100') def test_deleted_supporter(self): config['motion_min_supporters'] = 1