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
|
2012-07-13 11:16:06 +02:00
|
|
|
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
|
2013-01-26 16:33:55 +01:00
|
|
|
from django.utils.translation import ugettext_lazy, ugettext_noop, ugettext as _
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2012-07-08 09:21:39 +02:00
|
|
|
from openslides.utils.utils import _propper_unicode
|
2012-08-07 22:43:57 +02:00
|
|
|
from openslides.utils.person import PersonField
|
2012-07-08 09:21:39 +02:00
|
|
|
from openslides.config.models import config
|
2012-07-13 11:16:06 +02:00
|
|
|
from openslides.config.signals import default_config_value
|
2012-11-24 14:01:21 +01:00
|
|
|
from openslides.poll.models import (
|
|
|
|
BaseOption, BasePoll, CountVotesCast, CountInvalid, BaseVote)
|
|
|
|
from openslides.participant.models import User
|
2012-07-08 09:21:39 +02:00
|
|
|
from openslides.projector.api import register_slidemodel
|
|
|
|
from openslides.projector.models import SlideMixin
|
|
|
|
from openslides.agenda.models import Item
|
2012-06-23 11:41:32 +02:00
|
|
|
|
2013-02-01 16:44:06 +01:00
|
|
|
from .workflow import motion_workflow_choices, get_state, State, WorkflowError
|
2013-02-01 16:33:45 +01:00
|
|
|
|
2012-02-03 23:12:28 +01:00
|
|
|
|
2013-01-26 15:25:54 +01:00
|
|
|
# TODO: Save submitter and supporter in the same table
|
|
|
|
class MotionSubmitter(models.Model):
|
|
|
|
person = PersonField()
|
|
|
|
motion = models.ForeignKey('Motion', related_name="submitter")
|
2013-01-26 12:28:51 +01:00
|
|
|
|
2013-01-26 15:25:54 +01:00
|
|
|
def __unicode__(self):
|
|
|
|
return unicode(self.person)
|
2013-01-26 12:28:51 +01:00
|
|
|
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-26 15:25:54 +01:00
|
|
|
class MotionSupporter(models.Model):
|
2013-01-06 12:07:37 +01:00
|
|
|
person = PersonField()
|
2013-01-26 15:25:54 +01:00
|
|
|
motion = models.ForeignKey('Motion', related_name="supporter")
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
return unicode(self.person)
|
2013-01-06 12:07:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
class Motion(SlideMixin, models.Model):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
The Motion-Model.
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
prefix = "motion" # Rename this in the slide-system
|
|
|
|
|
|
|
|
# TODO: Use this attribute for the default_version, if the permission system
|
|
|
|
# is deactivated. Maybe it has to be renamed.
|
|
|
|
permitted_version = models.ForeignKey(
|
|
|
|
'MotionVersion', null=True, blank=True, related_name="permitted")
|
2013-02-01 16:33:45 +01:00
|
|
|
state_id = models.CharField(max_length=3)
|
2013-01-06 12:07:37 +01:00
|
|
|
# Log (Translatable)
|
|
|
|
identifier = models.CharField(max_length=255, null=True, blank=True,
|
|
|
|
unique=True)
|
|
|
|
category = models.ForeignKey('Category', null=True, blank=True)
|
|
|
|
# TODO proposal
|
|
|
|
# Maybe rename to master_copy
|
|
|
|
master = models.ForeignKey('self', null=True, blank=True)
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
class Meta:
|
|
|
|
permissions = (
|
|
|
|
('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')),
|
|
|
|
)
|
|
|
|
# TODO: order per default by category and identifier
|
|
|
|
# ordering = ('number',)
|
2012-08-04 12:19:31 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def __unicode__(self):
|
|
|
|
return self.get_title()
|
|
|
|
|
|
|
|
# TODO: Use transaction
|
|
|
|
def save(self, *args, **kwargs):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
Saves the motion. Create or update a motion_version object
|
|
|
|
"""
|
2013-02-01 16:33:45 +01:00
|
|
|
if not self.state_id:
|
|
|
|
self.state = 'default'
|
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
super(Motion, self).save(*args, **kwargs)
|
2013-01-26 15:39:35 +01:00
|
|
|
for attr in ['title', 'text', 'reason']:
|
|
|
|
if getattr(self, attr) != getattr(self.last_version, attr):
|
2013-01-06 12:07:37 +01:00
|
|
|
new_data = True
|
|
|
|
break
|
2013-01-26 15:39:35 +01:00
|
|
|
else:
|
|
|
|
new_data = False
|
|
|
|
|
2013-01-26 17:09:13 +01:00
|
|
|
need_new_version = config['motion_create_new_version'] == 'ALLWASY_CREATE_NEW_VERSION'
|
|
|
|
if hasattr(self, '_new_version') or (new_data and need_new_version):
|
2013-01-06 12:07:37 +01:00
|
|
|
version = self.new_version
|
|
|
|
del self._new_version
|
|
|
|
version.motion = self # Test if this line is realy neccessary.
|
|
|
|
elif new_data and not need_new_version:
|
|
|
|
# TODO: choose an explicit version
|
|
|
|
version = self.last_version
|
2011-09-06 22:22:29 +02:00
|
|
|
else:
|
2013-01-06 12:07:37 +01:00
|
|
|
# We do not need to save the motion version
|
2011-09-04 10:49:17 +02:00
|
|
|
return
|
2013-01-06 12:07:37 +01:00
|
|
|
for attr in ['title', 'text', 'reason']:
|
|
|
|
_attr = '_%s' % attr
|
2011-07-31 10:46:29 +02:00
|
|
|
try:
|
2013-01-06 12:07:37 +01:00
|
|
|
setattr(version, attr, getattr(self, _attr))
|
2011-07-31 10:46:29 +02:00
|
|
|
except AttributeError:
|
2013-01-06 12:07:37 +01:00
|
|
|
setattr(version, attr, getattr(self.last_version, attr))
|
|
|
|
version.save()
|
2012-02-14 16:31:21 +01:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def get_absolute_url(self, link='detail'):
|
|
|
|
if link == 'view' or link == 'detail':
|
|
|
|
return reverse('motion_detail', args=[str(self.id)])
|
2012-06-03 09:35:26 +02:00
|
|
|
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
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def get_title(self):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
Get the title of the motion. The titel is taken from motion.version
|
|
|
|
"""
|
2011-07-31 10:46:29 +02:00
|
|
|
try:
|
2013-01-06 12:07:37 +01:00
|
|
|
return self._title
|
2011-07-31 10:46:29 +02:00
|
|
|
except AttributeError:
|
2013-01-26 12:28:51 +01:00
|
|
|
return self.version.title
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def set_title(self, title):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
Set the titel of the motion. The titel will me saved in motion.save()
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
self._title = title
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
title = property(get_title, set_title)
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def get_text(self):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
Get the text of the motion. Simular to get_title()
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
try:
|
|
|
|
return self._text
|
|
|
|
except AttributeError:
|
2013-01-26 12:28:51 +01:00
|
|
|
return self.version.text
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def set_text(self, text):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
Set the text of the motion. Simular to set_title()
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
self._text = text
|
|
|
|
|
|
|
|
text = property(get_text, set_text)
|
|
|
|
|
|
|
|
def get_reason(self):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
Get the reason of the motion. Simular to get_title()
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
try:
|
|
|
|
return self._reason
|
|
|
|
except AttributeError:
|
2013-01-26 12:28:51 +01:00
|
|
|
return self.version.reason
|
2013-01-06 12:07:37 +01:00
|
|
|
|
|
|
|
def set_reason(self, reason):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
Set the reason of the motion. Simular to set_title()
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
self._reason = reason
|
|
|
|
|
|
|
|
reason = property(get_reason, set_reason)
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-26 12:28:51 +01:00
|
|
|
@property
|
|
|
|
def new_version(self):
|
|
|
|
"""
|
|
|
|
On the first call, it creates a new version. On any later call, it
|
|
|
|
use the existing new version.
|
|
|
|
|
|
|
|
The new_version object will be deleted when it is saved into the db
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return self._new_version
|
|
|
|
except AttributeError:
|
|
|
|
self._new_version = MotionVersion(motion=self)
|
|
|
|
return self._new_version
|
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def get_version(self, version_id):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
Return a specific version from version_id
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
# TODO: Check case, if version_id is not one of this motion
|
|
|
|
return self.versions.get(pk=version_id)
|
2012-07-13 11:16:06 +02:00
|
|
|
|
|
|
|
|
2013-01-26 12:28:51 +01:00
|
|
|
def get_version(self):
|
|
|
|
"""
|
|
|
|
Get the "active" version object. This version will be used to get the
|
|
|
|
data for this motion.
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
try:
|
2013-01-26 12:28:51 +01:00
|
|
|
return self._version
|
2013-01-06 12:07:37 +01:00
|
|
|
except AttributeError:
|
|
|
|
# TODO: choose right version via config
|
|
|
|
return self.last_version
|
2012-02-14 16:31:21 +01:00
|
|
|
|
2013-01-26 12:28:51 +01:00
|
|
|
def set_version(self, version):
|
|
|
|
"""
|
|
|
|
Set the "active" version object.
|
|
|
|
|
|
|
|
If version is None, the last_version will be used.
|
|
|
|
If version is a version object, this object will be used.
|
|
|
|
If version is Int, the N version of this motion will be used.
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
if version is None:
|
|
|
|
try:
|
2013-01-26 12:28:51 +01:00
|
|
|
del self._version
|
2013-01-06 12:07:37 +01:00
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
if type(version) is int:
|
|
|
|
version = self.versions.all()[version]
|
|
|
|
elif type(version) is not MotionVersion:
|
|
|
|
raise ValueError('The argument \'version\' has to be int or '
|
|
|
|
'MotionVersion, not %s' % type(version))
|
2013-01-26 12:28:51 +01:00
|
|
|
# TODO: Test, that the version is one of this motion
|
|
|
|
self._version = version
|
2012-02-14 16:31:21 +01:00
|
|
|
|
2013-01-26 12:28:51 +01:00
|
|
|
version = property(get_version, set_version)
|
2012-02-15 12:36:50 +01:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
@property
|
|
|
|
def last_version(self):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
|
|
|
Get the newest version of the motion
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
# TODO: Fix the case, that the motion has no Version
|
|
|
|
try:
|
|
|
|
return self.versions.order_by('id').reverse()[0]
|
|
|
|
except IndexError:
|
|
|
|
return self.new_version
|
2012-02-15 13:44:55 +01:00
|
|
|
|
2013-01-26 18:18:02 +01:00
|
|
|
def is_supporter(self, person):
|
|
|
|
try:
|
|
|
|
return self.supporter.filter(person=person).exists()
|
|
|
|
except AttributeError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
def support(self, person):
|
|
|
|
"""
|
|
|
|
Add a Supporter to the list of supporters of the motion.
|
|
|
|
"""
|
2013-02-01 16:44:06 +01:00
|
|
|
if self.state.support:
|
|
|
|
if not self.is_supporter(person):
|
|
|
|
MotionSupporter(motion=self, person=person).save()
|
|
|
|
#self.writelog(_("Supporter: +%s") % (person))
|
|
|
|
# TODO: Raise a precise exception for the view in else-clause
|
|
|
|
else:
|
|
|
|
raise WorkflowError("You can not support a motion in state %s" % self.state.name)
|
2013-01-26 18:18:02 +01:00
|
|
|
|
|
|
|
def unsupport(self, person):
|
|
|
|
"""
|
2013-02-01 16:44:06 +01:00
|
|
|
Remove a supporter from the list of supporters of the motion
|
2013-01-26 18:18:02 +01:00
|
|
|
"""
|
2013-02-01 16:44:06 +01:00
|
|
|
if self.state.support:
|
|
|
|
try:
|
|
|
|
self.supporter.filter(person=person).delete()
|
|
|
|
except MotionSupporter.DoesNotExist:
|
|
|
|
# TODO: Don't do nothing but raise a precise exception for the view
|
|
|
|
pass
|
|
|
|
#else:
|
|
|
|
#self.writelog(_("Supporter: -%s") % (person))
|
|
|
|
else:
|
|
|
|
raise WorkflowError("You can not unsupport a motion in state %s" % self.state.name)
|
2013-01-26 18:18:02 +01:00
|
|
|
|
2013-02-01 12:51:54 +01:00
|
|
|
def create_poll(self):
|
2013-02-01 16:33:45 +01:00
|
|
|
"""
|
|
|
|
Create a new poll for this motion
|
|
|
|
"""
|
2013-02-01 12:51:54 +01:00
|
|
|
# TODO: auto increment the poll_number in the Database
|
2013-02-01 16:44:06 +01:00
|
|
|
if self.state.poll:
|
|
|
|
poll_number = self.polls.aggregate(Max('poll_number'))['poll_number__max'] or 0
|
|
|
|
poll = MotionPoll.objects.create(motion=self, poll_number=poll_number + 1)
|
|
|
|
poll.set_options()
|
|
|
|
return poll
|
|
|
|
else:
|
|
|
|
raise WorkflowError("You can not create a poll in state %s" % self.state.name)
|
2013-02-01 12:51:54 +01:00
|
|
|
|
2013-02-01 16:33:45 +01:00
|
|
|
def get_state(self):
|
|
|
|
"""
|
|
|
|
Get the state of this motion. Return a State object.
|
|
|
|
"""
|
|
|
|
return get_state(self.state_id)
|
|
|
|
|
|
|
|
def set_state(self, state):
|
|
|
|
"""
|
|
|
|
Set the state of this motion.
|
|
|
|
|
|
|
|
state has to be a valid state id or State object.
|
|
|
|
"""
|
|
|
|
if type(state) is not State:
|
|
|
|
state = get_state(state)
|
|
|
|
self.state_id = state.id
|
|
|
|
|
|
|
|
state = property(get_state, set_state)
|
|
|
|
|
2012-02-15 13:44:55 +01:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
class MotionVersion(models.Model):
|
2013-01-26 16:33:55 +01:00
|
|
|
title = models.CharField(max_length=255, verbose_name=ugettext_lazy("Title"))
|
2013-01-06 12:07:37 +01:00
|
|
|
text = models.TextField(verbose_name=_("Text"))
|
2013-01-26 16:33:55 +01:00
|
|
|
reason = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Reason"))
|
2013-01-06 12:07:37 +01:00
|
|
|
rejected = models.BooleanField(default=False)
|
|
|
|
creation_time = models.DateTimeField(auto_now=True)
|
|
|
|
motion = models.ForeignKey(Motion, related_name='versions')
|
2013-01-26 16:33:55 +01:00
|
|
|
identifier = models.CharField(max_length=255, verbose_name=ugettext_lazy("Version identifier"))
|
2013-01-06 12:07:37 +01:00
|
|
|
note = models.TextField(null=True, blank=True)
|
2012-02-19 17:31:17 +01:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def __unicode__(self):
|
2013-01-26 15:25:54 +01:00
|
|
|
return "%s Version %s" % (self.motion, self.version_number)
|
2013-01-06 12:07:37 +01:00
|
|
|
|
2013-01-26 15:25:54 +01:00
|
|
|
def get_absolute_url(self, link='detail'):
|
|
|
|
if link == 'view' or link == 'detail':
|
|
|
|
return reverse('motion_version_detail', args=[str(self.motion.id),
|
|
|
|
str(self.version_number)])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def version_number(self):
|
2013-01-06 12:07:37 +01:00
|
|
|
if self.pk is None:
|
|
|
|
return 'new'
|
|
|
|
return (MotionVersion.objects.filter(motion=self.motion)
|
|
|
|
.filter(id__lte=self.pk).count())
|
2012-04-12 16:21:30 +02:00
|
|
|
|
2012-04-14 12:52:56 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
class Category(models.Model):
|
2013-01-26 16:33:55 +01:00
|
|
|
name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name"))
|
|
|
|
prefix = models.CharField(max_length=32, verbose_name=ugettext_lazy("Category prefix"))
|
2013-01-06 12:07:37 +01:00
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
return self.name
|
2012-04-14 17:09:15 +02:00
|
|
|
|
2012-04-14 12:52:56 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
class Comment(models.Model):
|
|
|
|
motion_version = models.ForeignKey(MotionVersion)
|
|
|
|
text = models.TextField()
|
|
|
|
author = PersonField()
|
|
|
|
creation_time = models.DateTimeField(auto_now=True)
|
2013-02-01 12:51:54 +01:00
|
|
|
|
|
|
|
|
|
|
|
class MotionVote(BaseVote):
|
|
|
|
option = models.ForeignKey('MotionOption')
|
|
|
|
|
|
|
|
|
|
|
|
class MotionOption(BaseOption):
|
|
|
|
poll = models.ForeignKey('MotionPoll')
|
|
|
|
vote_class = MotionVote
|
|
|
|
|
|
|
|
|
|
|
|
class MotionPoll(CountInvalid, CountVotesCast, BasePoll):
|
|
|
|
option_class = MotionOption
|
|
|
|
vote_values = [
|
|
|
|
ugettext_noop('Yes'), ugettext_noop('No'), ugettext_noop('Abstain')]
|
|
|
|
|
|
|
|
motion = models.ForeignKey(Motion, related_name='polls')
|
|
|
|
poll_number = models.PositiveIntegerField(default=1)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
unique_together = ("motion", "poll_number")
|
|
|
|
|
2013-02-01 13:24:44 +01:00
|
|
|
def __unicode__(self):
|
|
|
|
return _('Ballot %d') % self.poll_number
|
|
|
|
|
2013-02-01 12:51:54 +01:00
|
|
|
def get_absolute_url(self, link='edit'):
|
2013-02-01 13:24:44 +01:00
|
|
|
if link == 'edit':
|
|
|
|
return reverse('motion_poll_edit', args=[str(self.motion.pk),
|
|
|
|
str(self.poll_number)])
|
|
|
|
if link == 'delete':
|
|
|
|
return reverse('motion_poll_delete', args=[str(self.motion.pk),
|
|
|
|
str(self.poll_number)])
|
2013-02-01 12:51:54 +01:00
|
|
|
|
|
|
|
def get_motion(self):
|
|
|
|
return self.motion
|
|
|
|
|
|
|
|
def set_options(self):
|
|
|
|
#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)
|
|
|
|
|
|
|
|
def get_ballot(self):
|
|
|
|
return self.motion.motionpoll_set.filter(id__lte=self.id).count()
|