Merge pull request #1248 from emanuelschuetze/fix-1102

New poll config option for 100% base (fixed #1102 and #1240)
This commit is contained in:
Emanuel Schütze 2014-04-28 22:08:40 +02:00
commit 1fc0803846
17 changed files with 212 additions and 82 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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))

View File

@ -187,8 +187,8 @@
<td>
{% if candidate in assignment.elected %}
{% if perms.assignment.can_manage_assignment %}
<a class="election_link elected" href="{% url 'assignment_user_not_elected' assignment.id candidate.person_id %}"
rel="tooltip" data-original-title="{% trans 'Mark candidate as elected' %}"></a>
<a class="election_link elected tooltip-bottom" href="{% url 'assignment_user_not_elected' assignment.id candidate.person_id %}"
data-original-title="{% trans 'Mark candidate as elected' %}"></a>
{% else %}
<a class="elected">
<img src="{% static 'img/voting-yes.png' %}" class="tooltip-bottom" data-original-title="{% trans 'Candidate is elected' %}">
@ -196,7 +196,8 @@
{% endif %}
{% else %}
{% if perms.assignment.can_manage_assignment %}
<a class="election_link" href="{% url 'assignment_user_elected' assignment.id candidate.person_id %}"></a>
<a class="election_link tooltip-bottom" href="{% url 'assignment_user_elected' assignment.id candidate.person_id %}"
data-original-title="{% trans 'Mark candidate as elected' %}"></a>
{% endif %}
{% endif %}
<a href="{{ candidate|absolute_url }}">{{ candidate }}</a>
@ -221,13 +222,29 @@
{% endif %}
</tr>
{% endfor %}
<tr>
<td>{% trans 'Valid votes' %}</td>
{% for poll in polls %}
{% if poll.published or perms.assignment.can_manage_assignment %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-yes-grey.png' %}" class="tooltip-left" data-original-title="{% trans 'Valid votes' %}">
{{ poll.print_votesvalid }}
{% endif %}
</td>
{% endif %}
{% endfor %}
{% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %}
<td></td>
{% endif %}
</tr>
<tr>
<td>{% trans 'Invalid votes' %}</td>
{% for poll in polls %}
{% if poll.published or perms.assignment.can_manage_assignment %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-invalid.png' %}" class="tooltip-left" data-original-title="{% trans 'Invalid' %}">
<img src="{% static 'img/voting-invalid.png' %}" class="tooltip-left" data-original-title="{% trans 'Invalid votes' %}">
{{ poll.print_votesinvalid }}
{% endif %}
</td>
@ -238,13 +255,13 @@
{% endif %}
</tr>
<tr class="info total">
<td><strong>{% trans 'Votes cast' %}</strong></td>
<td>{% trans 'Votes cast' %}</td>
{% for poll in polls %}
{% if poll.published or perms.assignment.can_manage_assignment %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-total.png' %}" class="tooltip-left" data-original-title="{% trans 'Votes cast' %}">
<strong>{{ poll.print_votescast }}</strong>
{{ poll.print_votescast }}
{% endif %}
</td>
{% endif %}

View File

@ -50,6 +50,16 @@
{% endfor %}
</tr>
{% endfor %}
<tr>
<td>{% trans "Valid votes" %}</td>
{% for value in poll.get_vote_values %}
{% if forloop.first %}
<td>{{ pollform.votesvalid.errors }}{{ pollform.votesvalid }}</td>
{% else %}
<td></td>
{% endif %}
{% endfor %}
</tr>
<tr>
<td>{% trans "Invalid votes" %}</td>
{% for value in poll.get_vote_values %}

View File

@ -80,12 +80,23 @@
{% endfor %}
</tr>
{% endfor %}
<tr class="total">
<td>{% trans 'Valid votes' %}</td>
{% for poll in polls %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-yes-grey.png' %}" title="{% trans 'Valid votes' %}">
{{ poll.print_votesvalid }}
{% endif %}
</td>
{% endfor %}
</tr>
<tr>
<td>{% trans 'Invalid votes' %}</td>
{% for poll in polls %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid' %}">
{% if poll.has_votes %}
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid votes' %}">
{{ poll.print_votesinvalid }}
{% endif %}
</td>
@ -94,13 +105,13 @@
</tr>
<tr class="total">
<td>
<strong>{% trans 'Votes cast' %}</strong>
{% trans 'Votes cast' %}
</td>
{% for poll in polls %}
<td style="white-space:nowrap;">
{% if poll.has_votes %}
<img src="{% static 'img/voting-total.png' %}" title="{% trans 'Votes cast' %}">
<strong>{{ poll.print_votescast }}</strong>
{{ poll.print_votescast }}
{% endif %}
</td>
{% endfor %}

View File

@ -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(

View File

@ -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):
"""

View File

@ -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 = "<br/>%s: %s" % (_("Valid votes"), poll.print_votesvalid())
if poll.votesinvalid is not None:
invalid = "<br/>%s: %s" % (_("Invalid votes"), poll.print_votesinvalid())
if poll.votescast is not None:
votescast = "<br/>%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 <br/> %s: %s <br/> %s: %s <br/> %s: %s <br/> %s: %s" %
(_("Yes"), yes, _("No"), no, _("Abstention"), abstain, _("Invalid"),
invalid, _("Votes cast"), votecast), stylesheet['Normal']))
"%s: %s <br/> %s: %s <br/> %s: %s <br/> %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])

View File

@ -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(

View File

@ -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;
}

View File

@ -221,10 +221,21 @@
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Yes' %}"> {{ option.Yes }}<br>
<img src="{% static 'img/voting-no.png' %}" title="{% trans 'No' %}"> {{ option.No }}<br>
<img src="{% static 'img/voting-abstention.png' %}" title="{% trans 'Abstention' %}"> {{ option.Abstain }}<br>
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid' %}"> {{ poll.print_votesinvalid }}<br>
<div style="border-top: 1px solid; padding-top: 5px; margin: 5px 0; width: 10em;">
<img src="{% static 'img/voting-total.png' %}" title="{% trans 'Votes cast' %}"> {{ poll.print_votescast }}
</div>
{% if poll.votesvalid != None or poll.votesinvalid != None %}
<div class="resultline">
{% if poll.votesvalid != None %}
<img src="{% static 'img/voting-yes-grey.png' %}" title="{% trans 'Valid votes' %}"> {{ poll.print_votesvalid }}<br>
{% endif %}
{% if poll.votesinvalid != None %}
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid votes' %}"> {{ poll.print_votesinvalid }}<br>
{% endif %}
</div>
{% endif %}
{% if poll.votescast != None %}
<div class="resultline">
<img src="{% static 'img/voting-total.png' %}" title="{% trans 'Votes cast' %}"> {{ poll.print_votescast }}
</div>
{% endif %}
{% endwith %}
{% else %}
{% if perms.motion.can_manage_motion %}

View File

@ -45,6 +45,10 @@
</tr>
{% endfor %}
<tr class="total">
<td>{% trans "Valid votes" %}</td>
<td>{{ pollform.votesvalid.errors }}{{ pollform.votesvalid }}</td>
</tr>
<tr class="">
<td>{% trans "Invalid votes" %}</td>
<td>{{ pollform.votesinvalid.errors }}{{ pollform.votesinvalid }}</td>
</tr>

View File

@ -23,9 +23,19 @@
<img src="{% static 'img/voting-yes.png' %}" title="{% trans 'Yes' %}"> {{ option.Yes }} <br>
<img src="{% static 'img/voting-no.png' %}" title="{% trans 'No' %}"> {{ option.No }} <br>
<img src="{% static 'img/voting-abstention.png' %}" title="{% trans 'Abstention' %}"> {{ option.Abstain }}<br>
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid' %}"> {{ poll.print_votesinvalid }}<br>
<hr>
<img src="{% static 'img/voting-total.png' %}" title="{% trans 'Votes cast' %}"> {{ poll.print_votescast }}
{% if poll.votesvalid != None or poll.votesinvalid != None %}
<div class="resultline"></div>
{% if poll.votesvalid != None %}
<img src="{% static 'img/voting-yes-grey.png' %}" title="{% trans 'Valid votes' %}"> {{ poll.print_votesvalid }}<br>
{% endif %}
{% if poll.votesinvalid != None %}
<img src="{% static 'img/voting-invalid.png' %}" title="{% trans 'Invalid votes' %}"> {{ poll.print_votesinvalid }}<br>
{% endif %}
{% endif %}
{% if poll.votescast != None %}
<div class="resultline"></div>
<img src="{% static 'img/voting-total.png' %}" title="{% trans 'Votes cast' %}"> {{ poll.print_votescast }}
{% endif %}
</div>
{% endwith %}
{% else %}

View File

@ -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

View File

@ -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;

View File

@ -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