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 fd26d1947..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,6 +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