OpenSlides/openslides/motion/models.py

613 lines
20 KiB
Python
Raw Normal View History

2011-07-31 10:46:29 +02:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
2012-10-24 11:04:23 +02:00
openslides.motion.models
2011-07-31 10:46:29 +02:00
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2012-10-24 11:04:23 +02:00
Models for the motion app.
2011-07-31 10:46:29 +02:00
2012-04-25 22:29:19 +02:00
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS.
2011-07-31 10:46:29 +02:00
:license: GNU GPL, see LICENSE for more details.
"""
from datetime import datetime
2012-04-12 16:21:30 +02:00
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import Max
from django.dispatch import receiver
2012-06-30 15:21:27 +02:00
from django.utils.translation import pgettext
from django.utils.translation import ugettext_lazy as _, ugettext_noop, ugettext
2011-07-31 10:46:29 +02:00
from openslides.utils.utils import _propper_unicode
from openslides.utils.person import PersonField
2011-07-31 10:46:29 +02:00
from openslides.config.models import config
from openslides.config.signals import default_config_value
2012-06-30 15:21:27 +02:00
from openslides.poll.models import (BaseOption, BasePoll, CountVotesCast,
CountInvalid, BaseVote)
from openslides.participant.models import User, Group
from openslides.projector.api import register_slidemodel
from openslides.projector.models import SlideMixin
from openslides.agenda.models import Item
2012-10-24 11:04:23 +02:00
class MotionSupporter(models.Model):
motion = models.ForeignKey("Motion")
person = PersonField()
2012-10-24 11:04:23 +02:00
class Motion(models.Model, SlideMixin):
prefix = "motion"
2011-07-31 10:46:29 +02:00
STATUS = (
('pub', _('Published')),
('per', _('Permitted')),
('acc', _('Accepted')),
('rej', _('Rejected')),
('wit', _('Withdrawed')),
('adj', _('Adjourned')),
('noc', _('Not Concerned')),
('com', _('Commited a bill')),
('nop', _('Rejected (not authorized)')),
('rev', _('Needs Review')), # Where is this status used?
2011-07-31 10:46:29 +02:00
#additional actions:
# edit
# delete
# setnumber
# support
# unsupport
# createitem
# activateitem
2011-07-31 10:46:29 +02:00
# genpoll
)
submitter = PersonField(verbose_name=_("Submitter"))
2011-07-31 10:46:29 +02:00
number = models.PositiveSmallIntegerField(blank=True, null=True,
unique=True)
status = models.CharField(max_length=3, choices=STATUS, default='pub')
permitted = models.ForeignKey('AVersion', related_name='permitted', \
null=True, blank=True)
log = models.TextField(blank=True, null=True)
@property
def last_version(self):
"""
2012-10-24 11:04:23 +02:00
Return last version of the motion.
2011-07-31 10:46:29 +02:00
"""
try:
2012-10-24 11:04:23 +02:00
return AVersion.objects.filter(motion=self).order_by('id') \
2011-07-31 10:46:29 +02:00
.reverse()[0]
except IndexError:
return None
2011-09-06 10:41:06 +02:00
@property
def public_version(self):
"""
2012-10-24 11:04:23 +02:00
Return permitted, if the motion was permitted, else last_version
2011-09-06 10:41:06 +02:00
"""
if self.permitted is not None:
return self.permitted
else:
return self.last_version
def accept_version(self, version, user = None):
"""
accept a Version
"""
2011-09-04 10:02:54 +02:00
self.permitted = version
self.save(nonewversion=True)
2011-09-04 10:02:54 +02:00
version.rejected = False
version.save()
self.writelog(_("Version %d authorized") % (version.aid, ),
user)
def reject_version(self, version, user = None):
if version.id > self.permitted.id:
version.rejected = True
version.save()
self.writelog(pgettext("Rejected means not authorized", "Version %d rejected")
% (version.aid, ), user)
return True
return False
2011-07-31 10:46:29 +02:00
@property
def versions(self):
"""
2012-10-24 11:04:23 +02:00
Return a list of all versions of the motion.
2011-07-31 10:46:29 +02:00
"""
2012-10-24 11:04:23 +02:00
return AVersion.objects.filter(motion=self)
2011-07-31 10:46:29 +02:00
@property
def creation_time(self):
"""
2012-10-24 11:04:23 +02:00
Return the time of the creation of the motion.
2011-07-31 10:46:29 +02:00
"""
try:
return self.versions[0].time
except IndexError:
return None
@property
def notes(self):
"""
2012-10-24 11:04:23 +02:00
Return some information of the motion.
2011-07-31 10:46:29 +02:00
"""
note = []
if self.status == "pub" and not self.enough_supporters:
2012-09-13 21:33:57 +02:00
note.append(ugettext("Searching for supporters."))
2011-07-31 10:46:29 +02:00
if self.status == "pub" and self.permitted is None:
2012-09-13 21:33:57 +02:00
note.append(ugettext("Not yet authorized."))
2011-07-31 10:46:29 +02:00
elif self.unpermitted_changes and self.permitted:
2012-09-13 21:33:57 +02:00
note.append(ugettext("Not yet authorized changes."))
2011-07-31 10:46:29 +02:00
return note
@property
def unpermitted_changes(self):
"""
2012-10-24 11:04:23 +02:00
Return True if the motion has unpermitted changes.
2011-07-31 10:46:29 +02:00
2012-10-24 11:04:23 +02:00
The motion has unpermitted changes, if the permitted-version
is not the lastone and the lastone is not rejected.
2011-07-31 10:46:29 +02:00
TODO: rename the property in unchecked__changes
"""
if (self.last_version != self.permitted
and not self.last_version.rejected):
2011-07-31 10:46:29 +02:00
return True
else:
return False
@property
def supporters(self):
2012-10-24 11:04:23 +02:00
for object in self.motionsupporter_set.all():
yield object.person
def is_supporter(self, person):
try:
2012-10-24 11:04:23 +02:00
return self.motionsupporter_set.filter(person=person).exists()
except AttributeError:
return False
2011-07-31 10:46:29 +02:00
@property
def enough_supporters(self):
"""
2012-10-24 11:04:23 +02:00
Return True, if the motion has enough supporters
2011-07-31 10:46:29 +02:00
"""
2012-10-24 11:04:23 +02:00
min_supporters = int(config['motion_min_supporters'])
if self.status == "pub":
return self.count_supporters() >= min_supporters
else:
return True
2011-07-31 10:46:29 +02:00
def count_supporters(self):
2012-10-24 11:04:23 +02:00
return self.motionsupporter_set.count()
@property
def missing_supporters(self):
"""
Return number of missing supporters
"""
2012-10-24 11:04:23 +02:00
min_supporters = int(config['motion_min_supporters'])
delta = min_supporters - self.count_supporters()
if delta > 0:
return delta
else:
return 0
def save(self, user=None, nonewversion=False, trivial_change=False):
2011-07-31 10:46:29 +02:00
"""
2012-10-24 11:04:23 +02:00
Save the Motion, and create a new AVersion if necessary
2011-07-31 10:46:29 +02:00
"""
2012-10-24 11:04:23 +02:00
super(Motion, self).save()
if nonewversion:
return
2011-07-31 10:46:29 +02:00
last_version = self.last_version
fields = ["text", "title", "reason"]
2011-07-31 10:46:29 +02:00
if last_version is not None:
changed_fields = [f for f in fields
if getattr(last_version, f) != getattr(self, f)]
if not changed_fields:
2011-07-31 10:46:29 +02:00
return # No changes
try:
if trivial_change and last_version is not None:
last_version.text = self.text
last_version.title = self.title
last_version.reason = self.reason
last_version.save()
meta = AVersion._meta
field_names = [unicode(meta.get_field(f).verbose_name)
for f in changed_fields]
self.writelog(_("Trivial changes to version %(version)d; "
"changed fields: %(changed_fields)s")
% dict(version = last_version.aid,
changed_fields = ", ".join(field_names)))
return # Done
2012-06-19 10:50:55 +02:00
version = AVersion(title=getattr(self, 'title', ''),
text=getattr(self, 'text', ''),
reason=getattr(self, 'reason', ''),
2012-10-24 11:04:23 +02:00
motion=self)
2012-06-19 10:50:55 +02:00
version.save()
self.writelog(_("Version %s created") % version.aid, user)
2012-10-24 11:04:23 +02:00
is_manager = user.has_perm('motion.can_manage_motion')
2011-07-31 10:46:29 +02:00
except AttributeError:
is_manager = False
2012-10-24 11:04:23 +02:00
supporters = self.motionsupporter_set.all()
2011-07-31 10:46:29 +02:00
if (self.status == "pub"
and supporters
2011-07-31 10:46:29 +02:00
and not is_manager):
supporters.delete()
2011-07-31 10:46:29 +02:00
self.writelog(_("Supporters removed"), user)
def reset(self, user):
"""
2012-10-24 11:04:23 +02:00
Reset the motion.
2011-07-31 10:46:29 +02:00
"""
self.status = "pub"
self.permitted = None
self.save()
self.writelog(_("Status reseted to: %s") % (self.get_status_display()), user)
def support(self, person):
2011-07-31 10:46:29 +02:00
"""
2012-10-24 11:04:23 +02:00
Add a Supporter to the list of supporters of the motion.
2011-07-31 10:46:29 +02:00
"""
if person == self.submitter:
# TODO: Use own Exception
raise NameError('Supporter can not be the submitter of a '
2012-10-24 11:04:23 +02:00
'motion.')
if not self.is_supporter(person):
2012-10-24 11:04:23 +02:00
MotionSupporter(motion=self, person=person).save()
self.writelog(_("Supporter: +%s") % (person))
# TODO: Raise a precise exception for the view in else-clause
2011-07-31 10:46:29 +02:00
2012-09-12 10:17:51 +02:00
def unsupport(self, person):
2011-07-31 10:46:29 +02:00
"""
2012-10-24 11:04:23 +02:00
remove a supporter from the list of supporters of the motion
2011-07-31 10:46:29 +02:00
"""
try:
2012-10-24 11:04:23 +02:00
object = self.motionsupporter_set.get(person=person).delete()
except MotionSupporter.DoesNotExist:
# TODO: Don't do nothing but raise a precise exception for the view
pass
else:
2012-09-12 10:17:51 +02:00
self.writelog(_("Supporter: -%s") % (person))
2011-07-31 10:46:29 +02:00
def set_number(self, number=None, user=None):
"""
2012-10-24 11:04:23 +02:00
Set a number for ths motion.
2011-07-31 10:46:29 +02:00
"""
if self.number is not None:
# TODO: Use own Exception
2012-10-24 11:04:23 +02:00
raise NameError('This motion has already a number.')
2011-07-31 10:46:29 +02:00
if number is None:
try:
2012-10-24 11:04:23 +02:00
number = Motion.objects.aggregate(Max('number')) \
2011-07-31 10:46:29 +02:00
['number__max'] + 1
except TypeError:
number = 1
self.number = number
self.save()
self.writelog(_("Number set: %s") % (self.number), user)
return self.number
def permit(self, user=None):
"""
2012-10-24 11:04:23 +02:00
Change the status of this motion to permit.
2011-07-31 10:46:29 +02:00
"""
self.set_status(user, "per")
aversion = self.last_version
if self.number is None:
self.set_number()
self.permitted = aversion
self.save()
self.writelog(_("Version %s authorized") % (aversion.aid), user)
2011-07-31 10:46:29 +02:00
return self.permitted
def notpermit(self, user=None):
"""
2012-10-24 11:04:23 +02:00
Change the status of this motion to 'not permitted (rejected)'.
2011-07-31 10:46:29 +02:00
"""
self.set_status(user, "nop")
#TODO: reject last version
aversion = self.last_version
#self.permitted = aversion
if self.number is None:
self.set_number()
self.save()
self.writelog(_("Version %s not authorized") % (self.last_version.aid), user)
2011-07-31 10:46:29 +02:00
def set_status(self, user, status, force=False):
"""
2012-10-24 11:04:23 +02:00
Set the status of the motion.
2011-07-31 10:46:29 +02:00
"""
error = True
2012-10-24 11:04:23 +02:00
for a, b in Motion.STATUS:
2011-07-31 10:46:29 +02:00
if status == a:
error = False
break
if error:
#TODO: Use the Right Error
raise NameError(_('%s is not a valid status.') % status)
2011-07-31 10:46:29 +02:00
if self.status == status:
#TODO: Use the Right Error
raise NameError(_('The motion status is already \'%s.\'') \
2011-07-31 10:46:29 +02:00
% self.status)
actions = []
actions = self.get_allowed_actions(user)
if status not in actions and not force:
#TODO: Use the Right Error
raise NameError(_(
'The motion status is: \'%(currentstatus)s\'. '
'You can not set the status to \'%(newstatus)s\'.') % {
'currentstatus': self.status,
'newstatus': status})
2011-07-31 10:46:29 +02:00
oldstatus = self.get_status_display()
self.status = status
self.save()
self.writelog(_("Status modified")+": %s -> %s" \
% (oldstatus, self.get_status_display()), user)
def get_allowed_actions(self, user):
2011-07-31 10:46:29 +02:00
"""
Return a list of all the allowed status.
"""
actions = []
2012-10-24 11:04:23 +02:00
# check if user allowed to withdraw an motion
2012-04-15 13:10:24 +02:00
if ((self.status == "pub"
and self.number
and user == self.submitter)
or (self.status == "pub"
and self.number
2012-10-24 11:04:23 +02:00
and user.has_perm("motion.can_manage_motion"))
2012-04-15 13:10:24 +02:00
or (self.status == "per"
and user == self.submitter)
or (self.status == "per"
2012-10-24 11:04:23 +02:00
and user.has_perm("motion.can_manage_motion"))):
2011-07-31 10:46:29 +02:00
actions.append("wit")
2012-10-24 11:04:23 +02:00
#Check if the user can review the motion
2012-04-15 13:10:24 +02:00
if (self.status == "rev"
and (self.submitter == user
2012-10-24 11:04:23 +02:00
or user.has_perm("motion.can_manage_motion"))):
actions.append("pub")
2012-04-15 13:10:24 +02:00
2012-10-24 11:04:23 +02:00
# Check if the user can support and unspoort the motion
if (self.status == "pub"
and user != self.submitter
and not self.is_supporter(user)):
actions.append("support")
if self.status == "pub" and self.is_supporter(user):
2011-07-31 10:46:29 +02:00
actions.append("unsupport")
2012-10-24 11:04:23 +02:00
#Check if the user can edit the motion
if (user == self.submitter \
and (self.status in ('pub', 'per'))) \
2012-10-24 11:04:23 +02:00
or user.has_perm("motion.can_manage_motion"):
2011-07-31 10:46:29 +02:00
actions.append("edit")
2012-10-24 11:04:23 +02:00
# Check if the user can delete the motion (admin, manager, owner)
# reworked as requiered in #100
if (user.has_perm("motion.can_delete_all_motions") or
2012-10-24 11:04:23 +02:00
(user.has_perm("motion.can_manage_motion") and
self.number is None) or
(self.submitter == user and self.number is None)):
2011-07-31 10:46:29 +02:00
actions.append("delete")
#For the rest, all actions need the manage permission
2012-10-24 11:04:23 +02:00
if not user.has_perm("motion.can_manage_motion"):
2011-07-31 10:46:29 +02:00
return actions
if self.status == "pub":
actions.append("nop")
actions.append("per")
if self.number == None:
actions.append("setnumber")
if self.status == "per":
actions.append("acc")
actions.append("rej")
actions.append("adj")
actions.append("noc")
actions.append("com")
actions.append("genpoll")
if self.unpermitted_changes:
actions.append("permitversion")
actions.append("rejectversion")
return actions
def delete(self, force=False):
"""
2012-10-24 11:04:23 +02:00
Delete the motion. It is not possible, if the motion has
2011-07-31 10:46:29 +02:00
allready a number
"""
if self.number and not force:
2012-10-24 11:04:23 +02:00
raise NameError('The motion has already a number. ' \
2011-07-31 10:46:29 +02:00
'You can not delete it.')
2012-07-04 11:00:58 +02:00
for item in Item.objects.filter(related_sid=self.sid):
item.delete()
2012-10-24 11:04:23 +02:00
super(Motion, self).delete()
2011-07-31 10:46:29 +02:00
def writelog(self, text, user=None):
if not self.log:
self.log = ""
self.log += u"%s | %s" % (datetime.now().strftime("%d.%m.%Y %H:%M:%S"), _propper_unicode(text))
2011-07-31 10:46:29 +02:00
if user is not None:
if isinstance(user, User):
self.log += u" (%s %s)" % (_("by"), _propper_unicode(user.username))
else:
self.log += u" (%s %s)" % (_("by"), _propper_unicode(str(user)))
2011-07-31 10:46:29 +02:00
self.log += "\n"
self.save()
def get_agenda_title(self):
return self.public_version.title
def get_agenda_title_supplement(self):
number = self.number or '<i>[%s]</i>' % ugettext('no number')
return '(%s %s)' % (ugettext('motion'), number)
2011-07-31 10:46:29 +02:00
def __getattr__(self, name):
"""
if name is title, text, reason or time,
2012-10-24 11:04:23 +02:00
Return this attribute from the newest version of the motion
2011-07-31 10:46:29 +02:00
"""
if name in ('title', 'text', 'reason', 'time', 'aid'):
2011-07-31 10:46:29 +02:00
try:
if name == 'aid':
2011-09-06 10:41:06 +02:00
return self.last_version.aid
return self.last_version.__dict__[name]
2011-07-31 10:46:29 +02:00
except TypeError:
raise AttributeError(name)
2011-07-31 10:46:29 +02:00
except AttributeError:
2011-09-06 10:41:06 +02:00
raise AttributeError(name)
2011-07-31 10:46:29 +02:00
raise AttributeError(name)
def gen_poll(self, user=None):
2011-07-31 10:46:29 +02:00
"""
2012-10-24 11:04:23 +02:00
Generates a poll object for the motion
2011-07-31 10:46:29 +02:00
"""
2012-10-24 11:04:23 +02:00
poll = MotionPoll(motion=self)
2011-07-31 10:46:29 +02:00
poll.save()
poll.set_options()
2011-07-31 10:46:29 +02:00
self.writelog(_("Poll created"), user)
return poll
2012-02-14 16:31:21 +01:00
@property
def polls(self):
2012-10-24 11:04:23 +02:00
return self.motionpoll_set.all()
2012-02-14 16:31:21 +01:00
@property
def results(self):
2012-04-15 12:11:03 +02:00
return self.get_poll_results()
def get_poll_results(self):
"""
Return a list of voting results
"""
results = []
2012-02-14 16:31:21 +01:00
for poll in self.polls:
for option in poll.get_options():
2012-04-15 12:11:03 +02:00
if option.get_votes().exists():
results.append((option['Yes'], option['No'],
option['Abstain'], poll.print_votesinvalid(),
poll.print_votescast()))
return results
2012-04-15 12:11:03 +02:00
2012-02-06 22:08:08 +01:00
def slide(self):
"""
2012-02-06 22:08:08 +01:00
return the slide dict
"""
2012-10-24 11:04:23 +02:00
data = super(Motion, self).slide()
data['motion'] = self
data['title'] = self.title
2012-10-24 11:04:23 +02:00
data['template'] = 'projector/Motion.html'
return data
2011-07-31 10:46:29 +02:00
def get_absolute_url(self, link='view'):
if link == 'view':
2012-10-24 11:04:23 +02:00
return reverse('motion_view', args=[str(self.id)])
if link == 'edit':
2012-10-24 11:04:23 +02:00
return reverse('motion_edit', args=[str(self.id)])
2011-07-31 10:46:29 +02:00
if link == 'delete':
2012-10-24 11:04:23 +02:00
return reverse('motion_delete', args=[str(self.id)])
2011-07-31 10:46:29 +02:00
def __unicode__(self):
try:
return self.last_version.title
except AttributeError:
return "no title jet"
class Meta:
permissions = (
2012-10-24 11:04:23 +02:00
('can_see_motion', ugettext_noop("Can see motions")),
('can_create_motion', ugettext_noop("Can create motions")),
('can_support_motion', ugettext_noop("Can support motions")),
('can_manage_motion', ugettext_noop("Can manage motions")),
2011-07-31 10:46:29 +02:00
)
ordering = ('number',)
2011-07-31 10:46:29 +02:00
class AVersion(models.Model):
title = models.CharField(max_length=100, verbose_name = _("Title"))
text = models.TextField(verbose_name = _("Text"))
reason = models.TextField(null=True, blank=True, verbose_name = _("Reason"))
2012-06-30 15:21:27 +02:00
rejected = models.BooleanField() # = Not Permitted
2011-07-31 10:46:29 +02:00
time = models.DateTimeField(auto_now=True)
2012-10-24 11:04:23 +02:00
motion = models.ForeignKey(Motion)
2011-07-31 10:46:29 +02:00
def __unicode__(self):
return "%s %s" % (self.id, self.title)
@property
def aid(self):
try:
return self._aid
except AttributeError:
self._aid = AVersion.objects \
2012-10-24 11:04:23 +02:00
.filter(motion=self.motion) \
2011-07-31 10:46:29 +02:00
.filter(id__lte=self.id).count()
return self._aid
2012-10-24 11:04:23 +02:00
register_slidemodel(Motion)
2012-02-14 16:31:21 +01:00
2012-10-24 11:04:23 +02:00
class MotionVote(BaseVote):
option = models.ForeignKey('MotionOption')
2012-10-24 11:04:23 +02:00
class MotionOption(BaseOption):
poll = models.ForeignKey('MotionPoll')
vote_class = MotionVote
2012-02-14 16:31:21 +01:00
2012-10-24 11:04:23 +02:00
class MotionPoll(BasePoll, CountInvalid, CountVotesCast):
option_class = MotionOption
vote_values = [ugettext_noop('Yes'), ugettext_noop('No'),
ugettext_noop('Abstain')]
2012-02-15 12:36:50 +01:00
2012-10-24 11:04:23 +02:00
motion = models.ForeignKey(Motion)
2012-10-24 11:04:23 +02:00
def get_motion(self):
return self.motion
def set_options(self):
2012-02-19 19:27:00 +01:00
#TODO: maybe it is possible with .create() to call this without poll=self
self.get_option_class()(poll=self).save()
def append_pollform_fields(self, fields):
CountInvalid.append_pollform_fields(self, fields)
CountVotesCast.append_pollform_fields(self, fields)
2012-04-12 16:21:30 +02:00
def get_absolute_url(self):
2012-10-24 11:04:23 +02:00
return reverse('motion_poll_view', args=[self.id])
def get_ballot(self):
2012-10-24 11:04:23 +02:00
return self.motion.motionpoll_set.filter(id__lte=self.id).count()
2012-10-24 11:04:23 +02:00
@receiver(default_config_value, dispatch_uid="motion_default_config")
def default_config(sender, key, **kwargs):
return {
2012-10-24 11:04:23 +02:00
'motion_min_supporters': 0,
'motion_preamble': _('The assembly may decide,'),
'motion_pdf_ballot_papers_selection': 'CUSTOM_NUMBER',
'motion_pdf_ballot_papers_number': '8',
'motion_pdf_title': _('Motions'),
'motion_pdf_preamble': '',
'motion_allow_trivial_change': False,
}.get(key)