OpenSlides/openslides/assignment/models.py
Oskar Hahn c4c9322321 Rework of assignment
fixes: #1429, #1345

A database recreation is needed
2015-02-12 10:14:10 +01:00

409 lines
13 KiB
Python

from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ugettext_noop
from openslides.agenda.models import Item, Speaker
from openslides.core.models import Tag
from openslides.config.api import config
from openslides.poll.models import (BaseOption, BasePoll, BaseVote,
CollectDefaultVotesMixin,
PublishPollMixin)
from openslides.projector.models import SlideMixin
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import AbsoluteUrlMixin
from openslides.utils.rest_api import RESTModelMixin
from openslides.users.models import User
class AssignmentRelatedUser(RESTModelMixin, models.Model):
"""
Many to Many table between an assignment and user.
"""
STATUS_CANDIDATE = 1
STATUS_ELECTED = 2
STATUS_BLOCKED = 3
STATUSES = (
(STATUS_CANDIDATE, ugettext_lazy('candidate')),
(STATUS_ELECTED, ugettext_lazy('elected')),
(STATUS_BLOCKED, ugettext_lazy('blocked')),
)
assignment = models.ForeignKey(
'Assignment',
db_index=True,
related_name='assignment_related_users')
user = models.ForeignKey(User, db_index=True)
status = models.IntegerField(
choices=STATUSES,
default=STATUS_CANDIDATE)
class Meta:
unique_together = ('assignment', 'user')
def __str__(self):
return "%s <-> %s" % (self.assignment, self.user)
def get_root_rest_element(self):
"""
Returns the assignment to this instance which is the root REST element.
"""
return self.assignment
class Assignment(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
slide_callback_name = 'assignment'
PHASE_SEARCH = 1
PHASE_VOTING = 2
PHASE_FINISHED = 3
PHASES = (
(PHASE_SEARCH, ugettext_lazy('Searching for candidates')),
(PHASE_VOTING, ugettext_lazy('Voting')),
(PHASE_FINISHED, ugettext_lazy('Finished')),
)
title = models.CharField(
max_length=100,
verbose_name=ugettext_lazy("Title"))
"""
Title of the assignment.
"""
description = models.TextField(
blank=True,
verbose_name=ugettext_lazy("Description"))
"""
Text to describe the assignment.
"""
open_posts = models.PositiveSmallIntegerField(
verbose_name=ugettext_lazy("Number of members to be elected"))
"""
The number of members to be elected.
"""
poll_description_default = models.CharField(
max_length=79,
blank=True,
verbose_name=ugettext_lazy("Default comment on the ballot paper"))
"""
Default text for the poll description.
"""
phase = models.IntegerField(
choices=PHASES,
default=PHASE_SEARCH)
"""
Phase in which the assignment is.
"""
related_users = models.ManyToManyField(
User,
through='AssignmentRelatedUser')
"""
Users that a candidates, elected or blocked as candidate.
See AssignmentRelatedUser for more infos.
"""
tags = models.ManyToManyField(Tag, blank=True)
"""
Tags for the assignment.
"""
items = GenericRelation(Item)
"""
Agenda items for this assignment.
"""
class Meta:
permissions = (
('can_see_assignments', ugettext_noop('Can see elections')),
('can_nominate_other', ugettext_noop('Can nominate another participant')),
('can_nominate_self', ugettext_noop('Can nominate oneself')),
('can_manage_assignments', ugettext_noop('Can manage elections')),
)
ordering = ('title', )
verbose_name = ugettext_noop('Election')
def __str__(self):
return self.title
def get_absolute_url(self, link='detail'):
"""
Returns absolute url to the assignment instance.
"""
if link == 'detail':
url = reverse('assignment_detail', args=[str(self.pk)])
elif link == 'update':
url = reverse('assignment_update', args=[str(self.pk)])
elif link == 'delete':
url = reverse('assignment_delete', args=[str(self.pk)])
else:
url = super().get_absolute_url(link)
return url
def get_slide_context(self, **context):
"""
Retuns the context to generate the assignment slide.
"""
return super().get_slide_context(
polls=self.polls.filter(published=True),
vote_results=self.vote_results(only_published=True),
**context)
@property
def candidates(self):
"""
Queryset that represents the candidates for the assignment.
"""
return self.related_users.filter(
assignmentrelateduser__status=AssignmentRelatedUser.STATUS_CANDIDATE)
@property
def elected(self):
"""
Queryset that represents all elected users for the assignment.
"""
return self.related_users.filter(
assignmentrelateduser__status=AssignmentRelatedUser.STATUS_ELECTED)
@property
def blocked(self):
"""
Queryset that represents all blocked users for the assignment.
"""
return self.related_users.filter(
assignmentrelateduser__status=AssignmentRelatedUser.STATUS_BLOCKED)
def is_candidate(self, user):
"""
Returns True if user is a candidate.
Costs one database query.
"""
return self.candidates.filter(pk=user.pk).exists()
def is_elected(self, user):
"""
Returns True if the user is elected for this assignment.
Costs one database query.
"""
return self.elected.filter(pk=user.pk).exists()
def is_blocked(self, user):
"""
Returns True if the user is blockt for candidature.
Costs one database query.
"""
return self.blocked.filter(pk=user.pk).exists()
def set_candidate(self, user):
"""
Adds the user as candidate.
"""
related_user, __ = self.assignment_related_users.update_or_create(
user=user,
defaults={'status': AssignmentRelatedUser.STATUS_CANDIDATE})
def set_elected(self, user):
"""
Makes user an elected user for this assignment.
"""
related_user, __ = self.assignment_related_users.update_or_create(
user=user,
defaults={'status': AssignmentRelatedUser.STATUS_ELECTED})
def set_blocked(self, user):
"""
Block user from this assignment, so he can not get an candidate.
"""
related_user, __ = self.assignment_related_users.update_or_create(
user=user,
defaults={'status': AssignmentRelatedUser.STATUS_BLOCKED})
def delete_related_user(self, user):
"""
Delete the connection from the assignment to the user.
"""
self.assignment_related_users.filter(user=user).delete()
def set_phase(self, phase):
"""
Sets the phase attribute of the assignment.
Raises a ValueError if the phase is not valide.
"""
if phase not in dict(self.PHASES):
raise ValueError("Invalid phase %s" % phase)
self.phase = phase
def create_poll(self):
"""
Creates an new poll for the assignment and adds all candidates to all
lists of speakers of related agenda items.
"""
candidates = self.candidates.all()
# Find out the method of the election
if config['assignment_poll_vote_values'] == 'votes':
yesnoabstain = False
elif config['assignment_poll_vote_values'] == 'yesnoabstain':
yesnoabstain = True
else:
# config['assignment_poll_vote_values'] == 'auto'
# candidates <= available posts -> yes/no/abstain
if len(candidates) <= (self.open_posts - self.elected.count()):
yesnoabstain = True
else:
yesnoabstain = False
# Create the poll with the candidates.
poll = self.polls.create(
description=self.poll_description_default,
yesnoabstain=yesnoabstain)
poll.set_options({'candidate': user} for user in candidates)
# Add all candidates to all agenda items for this assignment
# TODO: Try to do this in a bulk create
for item in self.items.all():
for candidate in self.candidates:
try:
Speaker.objects.add(candidate, item)
except OpenSlidesError:
# The Speaker is already on the list. Do nothing.
# TODO: Find a smart way not to catch the error concerning AnonymousUser.
pass
return poll
def vote_results(self, only_published):
"""
Returns a table represented as a list with all candidates from all
related polls and their vote results.
"""
vote_results_dict = SortedDict()
polls = self.polls.all()
if only_published:
polls = polls.filter(published=True)
# All PollOption-Objects related to this assignment
options = []
for poll in polls:
options += poll.get_options()
for option in options:
candidate = option.candidate
if candidate in vote_results_dict:
continue
vote_results_dict[candidate] = []
for poll in polls:
votes = {}
try:
# candidate related to this poll
poll_option = poll.get_options().get(candidate=candidate)
for vote in poll_option.get_votes():
votes[vote.value] = vote.print_weight()
except AssignmentOption.DoesNotExist:
# candidate not in related to this poll
votes = None
vote_results_dict[candidate].append(votes)
return vote_results_dict
def get_agenda_title(self):
return str(self)
def get_agenda_title_supplement(self):
return '(%s)' % _('Assignment')
class AssignmentVote(RESTModelMixin, BaseVote):
option = models.ForeignKey('AssignmentOption')
def get_root_rest_element(self):
"""
Returns the assignment to this instance which is the root REST element.
"""
return self.option.poll.assignment
class AssignmentOption(RESTModelMixin, BaseOption):
poll = models.ForeignKey('AssignmentPoll')
candidate = models.ForeignKey(User)
vote_class = AssignmentVote
def __str__(self):
return str(self.candidate)
def get_root_rest_element(self):
"""
Returns the assignment to this instance which is the root REST element.
"""
return self.poll.assignment
class AssignmentPoll(RESTModelMixin, SlideMixin, CollectDefaultVotesMixin,
PublishPollMixin, AbsoluteUrlMixin, BasePoll):
slide_callback_name = 'assignmentpoll'
option_class = AssignmentOption
assignment = models.ForeignKey(Assignment, related_name='polls')
yesnoabstain = models.BooleanField(default=False)
description = models.CharField(
max_length=79,
blank=True,
verbose_name=ugettext_lazy("Comment on the ballot paper"))
def __str__(self):
return _("Ballot %d") % self.get_ballot()
def get_absolute_url(self, link='update'):
"""
Return an URL for the poll.
The keyargument 'link' can be 'update' or 'delete'.
"""
if link == 'update':
url = reverse('assignmentpoll_update', args=[str(self.pk)])
elif link == 'delete':
url = reverse('assignmentpoll_delete', args=[str(self.pk)])
else:
url = super().get_absolute_url(link)
return url
def get_assignment(self):
return self.assignment
def get_vote_values(self):
if self.yesnoabstain:
return [ugettext_noop('Yes'), ugettext_noop('No'), ugettext_noop('Abstain')]
else:
return [ugettext_noop('Votes')]
def get_ballot(self):
return self.assignment.polls.filter(id__lte=self.pk).count()
def get_percent_base_choice(self):
return config['assignment_poll_100_percent_base']
def append_pollform_fields(self, fields):
fields.append('description')
super().append_pollform_fields(fields)
def get_slide_context(self, **context):
return super().get_slide_context(poll=self)
def get_root_rest_element(self):
"""
Returns the assignment to this instance which is the root REST element.
"""
return self.assignment