2015-04-30 19:13:28 +02:00
|
|
|
from django.conf import settings
|
2016-09-18 16:00:31 +02:00
|
|
|
from django.contrib.contenttypes.fields import GenericRelation
|
2013-09-25 10:01:01 +02:00
|
|
|
from django.db import models
|
2012-07-13 11:16:06 +02:00
|
|
|
from django.db.models import Max
|
2013-03-09 14:29:40 +01:00
|
|
|
from django.utils import formats
|
2013-09-25 10:01:01 +02:00
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from django.utils.translation import ugettext_lazy, ugettext_noop
|
2015-06-16 10:37:23 +02:00
|
|
|
from jsonfield import JSONField
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2015-10-24 19:02:43 +02:00
|
|
|
from openslides.agenda.models import Item
|
2015-06-29 12:08:15 +02:00
|
|
|
from openslides.core.config import config
|
2014-12-26 13:45:13 +01:00
|
|
|
from openslides.core.models import Tag
|
2015-03-26 05:36:10 +01:00
|
|
|
from openslides.mediafiles.models import Mediafile
|
2015-06-16 10:37:23 +02:00
|
|
|
from openslides.poll.models import (
|
|
|
|
BaseOption,
|
|
|
|
BasePoll,
|
|
|
|
BaseVote,
|
|
|
|
CollectDefaultVotesMixin,
|
|
|
|
)
|
2016-10-14 21:48:02 +02:00
|
|
|
from openslides.utils.autoupdate import inform_changed_data
|
2015-06-29 12:08:15 +02:00
|
|
|
from openslides.utils.models import RESTModelMixin
|
2016-01-03 15:33:51 +01:00
|
|
|
from openslides.utils.search import user_name_helper
|
2012-06-23 11:41:32 +02:00
|
|
|
|
2016-02-11 22:58:32 +01:00
|
|
|
from .access_permissions import (
|
|
|
|
CategoryAccessPermissions,
|
|
|
|
MotionAccessPermissions,
|
2016-10-01 20:42:44 +02:00
|
|
|
MotionBlockAccessPermissions,
|
2016-09-10 18:49:38 +02:00
|
|
|
MotionChangeRecommendationAccessPermissions,
|
2016-02-11 22:58:32 +01:00
|
|
|
WorkflowAccessPermissions,
|
|
|
|
)
|
2013-09-25 10:01:01 +02:00
|
|
|
from .exceptions import WorkflowError
|
2012-02-03 23:12:28 +01:00
|
|
|
|
2013-02-17 17:07:44 +01:00
|
|
|
|
2016-09-18 16:00:31 +02:00
|
|
|
class MotionManager(models.Manager):
|
2016-09-30 20:42:58 +02:00
|
|
|
"""
|
|
|
|
Customized model manager to support our get_full_queryset method.
|
|
|
|
"""
|
2016-09-18 16:00:31 +02:00
|
|
|
def get_full_queryset(self):
|
2016-09-30 20:42:58 +02:00
|
|
|
"""
|
|
|
|
Returns the normal queryset with all motions. In the background we
|
|
|
|
join and prefetch all related models.
|
|
|
|
"""
|
|
|
|
return (self.get_queryset()
|
2016-09-18 16:00:31 +02:00
|
|
|
.select_related('active_version')
|
|
|
|
.prefetch_related(
|
|
|
|
'versions',
|
|
|
|
'agenda_items',
|
|
|
|
'log_messages',
|
|
|
|
'polls',
|
|
|
|
'attachments',
|
|
|
|
'tags',
|
|
|
|
'submitters',
|
|
|
|
'supporters'))
|
|
|
|
|
|
|
|
|
2015-06-29 13:31:07 +02:00
|
|
|
class Motion(RESTModelMixin, models.Model):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2016-09-30 20:42:58 +02:00
|
|
|
Model for motions.
|
2013-01-26 12:28:51 +01:00
|
|
|
|
2013-02-05 18:46:46 +01:00
|
|
|
This class is the main entry point to all other classes related to a motion.
|
|
|
|
"""
|
2016-02-11 22:58:32 +01:00
|
|
|
access_permissions = MotionAccessPermissions()
|
2013-01-26 12:28:51 +01:00
|
|
|
|
2016-09-18 16:00:31 +02:00
|
|
|
objects = MotionManager()
|
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
active_version = models.ForeignKey(
|
|
|
|
'MotionVersion',
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
null=True,
|
|
|
|
related_name="active_version")
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Points to a specific version.
|
2013-01-26 15:25:54 +01:00
|
|
|
|
2013-03-14 22:54:00 +01:00
|
|
|
Used be the permitted-version-system to deside which version is the active
|
|
|
|
version. Could also be used to only choose a specific version as a default
|
|
|
|
version. Like the sighted versions on Wikipedia.
|
2013-02-05 18:46:46 +01:00
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
state = models.ForeignKey(
|
|
|
|
'State',
|
2016-09-03 21:43:11 +02:00
|
|
|
related_name='+',
|
2016-01-09 09:58:22 +01:00
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
null=True) # TODO: Check whether null=True is necessary.
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
The related state object.
|
2013-01-06 12:07:37 +01:00
|
|
|
|
2013-02-06 23:56:21 +01:00
|
|
|
This attribute is to get the current state of the motion.
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
|
2016-09-03 21:43:11 +02:00
|
|
|
recommendation = models.ForeignKey(
|
|
|
|
'State',
|
|
|
|
related_name='+',
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
null=True)
|
|
|
|
"""
|
|
|
|
The recommendation of a person or committee for this motion.
|
|
|
|
"""
|
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
identifier = models.CharField(max_length=255, null=True, blank=True,
|
|
|
|
unique=True)
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
A string as human readable identifier for the motion.
|
|
|
|
"""
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-03-12 22:03:56 +01:00
|
|
|
identifier_number = models.IntegerField(null=True)
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Counts the number of the motion in one category.
|
2013-03-12 22:03:56 +01:00
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
Needed to find the next free motion identifier.
|
2013-03-12 22:03:56 +01:00
|
|
|
"""
|
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
category = models.ForeignKey(
|
|
|
|
'Category',
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
null=True,
|
|
|
|
blank=True)
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
ForeignKey to one category of motions.
|
|
|
|
"""
|
2013-03-11 20:17:19 +01:00
|
|
|
|
2016-10-01 20:42:44 +02:00
|
|
|
motion_block = models.ForeignKey(
|
|
|
|
'MotionBlock',
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
null=True,
|
|
|
|
blank=True)
|
|
|
|
"""
|
|
|
|
ForeignKey to one block of motions.
|
|
|
|
"""
|
|
|
|
|
2016-07-13 14:45:40 +02:00
|
|
|
origin = models.CharField(max_length=255, blank=True)
|
|
|
|
"""
|
|
|
|
A string to describe the origin of this motion e. g. that it was
|
|
|
|
discussed at another assembly/conference.
|
|
|
|
"""
|
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
attachments = models.ManyToManyField(Mediafile, blank=True)
|
2013-10-16 23:57:29 +02:00
|
|
|
"""
|
|
|
|
Many to many relation to mediafile objects.
|
|
|
|
"""
|
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
parent = models.ForeignKey(
|
|
|
|
'self',
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
null=True,
|
|
|
|
blank=True,
|
|
|
|
related_name='amendments')
|
2014-12-25 10:58:52 +01:00
|
|
|
"""
|
|
|
|
Field for amendments to reference to the motion that should be altered.
|
|
|
|
|
|
|
|
Null if the motion is not an amendment.
|
|
|
|
"""
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
tags = models.ManyToManyField(Tag, blank=True)
|
2014-12-26 13:45:13 +01:00
|
|
|
"""
|
|
|
|
Tags to categorise motions.
|
|
|
|
"""
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
submitters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_submitters', blank=True)
|
2015-04-30 19:13:28 +02:00
|
|
|
"""
|
|
|
|
Users who submit this motion.
|
|
|
|
"""
|
|
|
|
|
2015-08-31 14:07:24 +02:00
|
|
|
supporters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_supporters', blank=True)
|
2015-04-30 19:13:28 +02:00
|
|
|
"""
|
|
|
|
Users who support this motion.
|
|
|
|
"""
|
|
|
|
|
2016-07-29 23:33:47 +02:00
|
|
|
comments = JSONField(null=True)
|
|
|
|
"""
|
|
|
|
Configurable fields for comments. Contains a list of strings.
|
|
|
|
"""
|
|
|
|
|
2016-09-30 20:42:58 +02:00
|
|
|
# In theory there could be one then more agenda_item. But we support only
|
|
|
|
# one. See the property agenda_item.
|
2016-09-18 16:00:31 +02:00
|
|
|
agenda_items = GenericRelation(Item, related_name='motions')
|
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
class Meta:
|
2015-12-10 00:20:59 +01:00
|
|
|
default_permissions = ()
|
2013-01-06 12:07:37 +01:00
|
|
|
permissions = (
|
2016-01-27 13:41:19 +01:00
|
|
|
('can_see', 'Can see motions'),
|
|
|
|
('can_create', 'Can create motions'),
|
|
|
|
('can_support', 'Can support motions'),
|
2016-07-29 23:33:47 +02:00
|
|
|
('can_see_and_manage_comments', 'Can see and manage comments'),
|
2016-01-27 13:41:19 +01:00
|
|
|
('can_manage', 'Can manage motions'),
|
2013-01-06 12:07:37 +01:00
|
|
|
)
|
2013-06-01 12:36:42 +02:00
|
|
|
ordering = ('identifier', )
|
2013-09-07 00:18:13 +02:00
|
|
|
verbose_name = ugettext_noop('Motion')
|
2012-08-04 12:19:31 +02:00
|
|
|
|
2014-08-16 09:25:18 +02:00
|
|
|
def __str__(self):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2016-01-25 21:22:22 +01:00
|
|
|
Return the title of this motion.
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2016-01-25 21:22:22 +01:00
|
|
|
return self.title
|
2013-01-06 12:07:37 +01:00
|
|
|
|
|
|
|
# TODO: Use transaction
|
2016-10-14 21:48:02 +02:00
|
|
|
def save(self, use_version=None, skip_autoupdate=False, *args, **kwargs):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Save the motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-02-06 23:56:21 +01:00
|
|
|
1. Set the state of a new motion to the default state.
|
2013-05-09 23:51:58 +02:00
|
|
|
2. Ensure that the identifier is not an empty string.
|
|
|
|
3. Save the motion object.
|
2013-06-02 22:23:58 +02:00
|
|
|
4. Save the version data.
|
|
|
|
5. Set the active version for the motion if a new version object was saved.
|
2013-06-01 12:36:42 +02:00
|
|
|
|
|
|
|
The version data is *not* saved, if
|
2013-06-02 22:23:58 +02:00
|
|
|
1. the django-feature 'update_fields' is used or
|
|
|
|
2. the argument use_version is False (differ to None).
|
2013-06-01 12:36:42 +02:00
|
|
|
|
2013-06-02 22:23:58 +02:00
|
|
|
The argument use_version is choose the version object into which the
|
|
|
|
version data is saved.
|
|
|
|
* If use_version is False, no version data is saved.
|
|
|
|
* If use_version is None, the last version is used.
|
|
|
|
* Else the given version is used.
|
2013-06-01 12:36:42 +02:00
|
|
|
|
2013-06-02 22:23:58 +02:00
|
|
|
To create and use a new version object, you have to set it via the
|
|
|
|
use_version argument. You have to set the title, text and reason into
|
|
|
|
this version object before giving it to this save method. The properties
|
|
|
|
motion.title, motion.text and motion.reason will be ignored.
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
2013-02-06 23:56:21 +01:00
|
|
|
if not self.state:
|
2013-02-01 17:20:11 +01:00
|
|
|
self.reset_state()
|
2013-06-01 12:36:42 +02:00
|
|
|
|
|
|
|
# Solves the problem, that there can only be one motion with an empty
|
2013-06-02 22:23:58 +02:00
|
|
|
# string as identifier.
|
2014-08-16 09:25:18 +02:00
|
|
|
if not self.identifier and isinstance(self.identifier, str):
|
2013-04-19 14:12:49 +02:00
|
|
|
self.identifier = None
|
|
|
|
|
2016-10-14 21:48:02 +02:00
|
|
|
# Always skip autoupdate. Maybe we run it later in this method.
|
|
|
|
super(Motion, self).save(skip_autoupdate=True, *args, **kwargs)
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-06-01 12:36:42 +02:00
|
|
|
if 'update_fields' in kwargs:
|
2013-06-02 22:23:58 +02:00
|
|
|
# Do not save the version data if only some motion fields are updated.
|
2016-10-14 21:48:02 +02:00
|
|
|
if not skip_autoupdate:
|
|
|
|
inform_changed_data(self)
|
2013-06-01 12:36:42 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
if use_version is False:
|
2013-06-02 22:23:58 +02:00
|
|
|
# We do not need to save the version.
|
2016-10-14 21:48:02 +02:00
|
|
|
if not skip_autoupdate:
|
|
|
|
inform_changed_data(self)
|
2013-06-01 12:36:42 +02:00
|
|
|
return
|
|
|
|
elif use_version is None:
|
|
|
|
use_version = self.get_last_version()
|
2013-06-02 22:23:58 +02:00
|
|
|
# Save title, text and reason into the version object.
|
2013-05-09 23:51:58 +02:00
|
|
|
for attr in ['title', 'text', 'reason']:
|
|
|
|
_attr = '_%s' % attr
|
2013-06-01 12:36:42 +02:00
|
|
|
data = getattr(self, _attr, None)
|
|
|
|
if data is not None:
|
|
|
|
setattr(use_version, attr, data)
|
2013-05-09 23:51:58 +02:00
|
|
|
delattr(self, _attr)
|
2013-06-01 12:36:42 +02:00
|
|
|
|
|
|
|
# If version is not in the database, test if it has new data and set
|
2013-06-02 22:23:58 +02:00
|
|
|
# the version_number.
|
2013-06-01 12:36:42 +02:00
|
|
|
if use_version.id is None:
|
|
|
|
if not self.version_data_changed(use_version):
|
2013-06-02 22:23:58 +02:00
|
|
|
# We do not need to save the version.
|
2016-10-14 21:48:02 +02:00
|
|
|
if not skip_autoupdate:
|
|
|
|
inform_changed_data(self)
|
2013-06-01 12:36:42 +02:00
|
|
|
return
|
|
|
|
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
|
|
|
|
use_version.version_number = version_number + 1
|
|
|
|
|
2013-06-02 22:23:58 +02:00
|
|
|
# Necessary line if the version was set before the motion got an id.
|
2013-06-01 12:36:42 +02:00
|
|
|
use_version.motion = use_version.motion
|
|
|
|
|
2016-10-14 21:48:02 +02:00
|
|
|
# Always skip autoupdate. Maybe we run it later in this method.
|
|
|
|
use_version.save(skip_autoupdate=True)
|
2013-06-01 12:36:42 +02:00
|
|
|
|
|
|
|
# Set the active version of this motion. This has to be done after the
|
2013-06-02 22:23:58 +02:00
|
|
|
# version is saved in the database.
|
|
|
|
# TODO: Move parts of these last lines of code outside the save method
|
2016-10-14 21:48:02 +02:00
|
|
|
# when other versions than the last one should be edited later on.
|
2013-06-01 12:36:42 +02:00
|
|
|
if self.active_version is None or not self.state.leave_old_version_active:
|
2013-06-19 21:59:03 +02:00
|
|
|
# TODO: Don't call this if it was not a new version
|
2013-06-01 12:36:42 +02:00
|
|
|
self.active_version = use_version
|
2016-10-14 21:48:02 +02:00
|
|
|
# Always skip autoupdate. Maybe we run it later in this method.
|
|
|
|
self.save(update_fields=['active_version'], skip_autoupdate=True)
|
|
|
|
|
|
|
|
# Finally run autoupdate if it is not skipped by caller.
|
|
|
|
if not skip_autoupdate:
|
|
|
|
inform_changed_data(self)
|
2013-02-03 13:24:29 +01:00
|
|
|
|
2013-06-01 12:36:42 +02:00
|
|
|
def version_data_changed(self, version):
|
|
|
|
"""
|
|
|
|
Compare the version with the last version of the motion.
|
|
|
|
|
2013-06-02 22:23:58 +02:00
|
|
|
Returns True if the version data (title, text, reason) is different,
|
|
|
|
else returns False.
|
2013-06-01 12:36:42 +02:00
|
|
|
"""
|
|
|
|
if not self.versions.exists():
|
2013-06-02 22:23:58 +02:00
|
|
|
# If there is no version in the database, the data has always changed.
|
2013-06-01 12:36:42 +02:00
|
|
|
return True
|
|
|
|
|
|
|
|
last_version = self.get_last_version()
|
|
|
|
for attr in ['title', 'text', 'reason']:
|
|
|
|
if getattr(last_version, attr) != getattr(version, attr):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2013-03-12 22:03:56 +01:00
|
|
|
def set_identifier(self):
|
2013-05-09 23:51:58 +02:00
|
|
|
"""
|
2014-12-25 10:58:52 +01:00
|
|
|
Sets the motion identifier automaticly according to the config value if
|
|
|
|
it is not set yet.
|
2013-05-09 23:51:58 +02:00
|
|
|
"""
|
2014-12-25 10:58:52 +01:00
|
|
|
# The identifier is already set or should be set manually
|
2015-06-16 18:12:59 +02:00
|
|
|
if config['motions_identifier'] == 'manually' or self.identifier:
|
2013-03-12 23:35:08 +01:00
|
|
|
# Do not set an identifier.
|
|
|
|
return
|
2014-12-25 10:58:52 +01:00
|
|
|
|
|
|
|
# The motion is an amendment
|
|
|
|
elif self.is_amendment():
|
|
|
|
motions = self.parent.amendments.all()
|
|
|
|
|
|
|
|
# The motions should be counted per category
|
2015-06-16 18:12:59 +02:00
|
|
|
elif config['motions_identifier'] == 'per_category':
|
2013-03-12 23:35:08 +01:00
|
|
|
motions = Motion.objects.filter(category=self.category)
|
2014-12-25 10:58:52 +01:00
|
|
|
|
|
|
|
# The motions should be counted over all.
|
|
|
|
else:
|
2013-03-12 23:35:08 +01:00
|
|
|
motions = Motion.objects.all()
|
|
|
|
|
|
|
|
number = motions.aggregate(Max('identifier_number'))['identifier_number__max'] or 0
|
2014-12-25 10:58:52 +01:00
|
|
|
if self.is_amendment():
|
|
|
|
parent_identifier = self.parent.identifier or ''
|
2015-06-16 18:12:59 +02:00
|
|
|
prefix = '%s %s ' % (parent_identifier, config['motions_amendments_prefix'])
|
2014-12-25 10:58:52 +01:00
|
|
|
elif self.category is None or not self.category.prefix:
|
2013-03-12 22:03:56 +01:00
|
|
|
prefix = ''
|
2013-03-12 23:35:08 +01:00
|
|
|
else:
|
2013-06-01 12:36:42 +02:00
|
|
|
prefix = '%s ' % self.category.prefix
|
2013-03-12 22:03:56 +01:00
|
|
|
|
2013-06-01 12:36:42 +02:00
|
|
|
number += 1
|
|
|
|
identifier = '%s%d' % (prefix, number)
|
|
|
|
while Motion.objects.filter(identifier=identifier).exists():
|
2013-03-12 22:03:56 +01:00
|
|
|
number += 1
|
2013-06-01 12:36:42 +02:00
|
|
|
identifier = '%s%d' % (prefix, number)
|
|
|
|
|
|
|
|
self.identifier = identifier
|
|
|
|
self.identifier_number = number
|
2013-03-12 22:03:56 +01:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def get_title(self):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Get the title of the motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-05-09 23:51:58 +02:00
|
|
|
The title is taken from motion.version.
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
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-06-01 12:36:42 +02:00
|
|
|
return self.get_active_version().title
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def set_title(self, title):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2013-11-12 10:57:07 +01:00
|
|
|
Set the title of the motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-06-02 22:23:58 +02:00
|
|
|
The title will be saved in the version object, when motion.save() is
|
2013-02-05 18:46:46 +01:00
|
|
|
called.
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
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)
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
The title of the motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
|
|
|
Is saved in a MotionVersion object.
|
|
|
|
"""
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def get_text(self):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Get the text of the motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
|
|
|
Simular to get_title().
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
try:
|
|
|
|
return self._text
|
|
|
|
except AttributeError:
|
2013-06-01 12:36:42 +02:00
|
|
|
return self.get_active_version().text
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
def set_text(self, text):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Set the text of the motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
|
|
|
Simular to set_title().
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
self._text = text
|
|
|
|
|
|
|
|
text = property(get_text, set_text)
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
The text of a motin.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
|
|
|
Is saved in a MotionVersion object.
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
|
|
|
|
def get_reason(self):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Get the reason of the motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
|
|
|
Simular to get_title().
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
try:
|
|
|
|
return self._reason
|
|
|
|
except AttributeError:
|
2013-06-01 12:36:42 +02:00
|
|
|
return self.get_active_version().reason
|
2013-01-06 12:07:37 +01:00
|
|
|
|
|
|
|
def set_reason(self, reason):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Set the reason of the motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
|
|
|
Simular to set_title().
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
self._reason = reason
|
|
|
|
|
|
|
|
reason = property(get_reason, set_reason)
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
The reason for the motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
|
|
|
Is saved in a MotionVersion object.
|
|
|
|
"""
|
2011-07-31 10:46:29 +02:00
|
|
|
|
2013-06-13 15:43:17 +02:00
|
|
|
def get_new_version(self, **kwargs):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Return a version object, not saved in the database.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-06-01 12:36:42 +02:00
|
|
|
The version data of the new version object is populated with the data
|
2013-06-13 15:43:17 +02:00
|
|
|
set via motion.title, motion.text, motion.reason if these data are
|
|
|
|
not given as keyword arguments. If the data is not set in the motion
|
|
|
|
attributes, it is populated with the data from the last version
|
|
|
|
object if such object exists.
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2015-06-30 02:37:57 +02:00
|
|
|
if self.pk is None:
|
|
|
|
# Do not reference the MotionVersion object to an unsaved motion
|
|
|
|
new_version = MotionVersion(**kwargs)
|
|
|
|
else:
|
|
|
|
new_version = MotionVersion(motion=self, **kwargs)
|
2013-06-01 12:36:42 +02:00
|
|
|
if self.versions.exists():
|
|
|
|
last_version = self.get_last_version()
|
|
|
|
else:
|
|
|
|
last_version = None
|
|
|
|
for attr in ['title', 'text', 'reason']:
|
2013-06-13 15:43:17 +02:00
|
|
|
if attr in kwargs:
|
|
|
|
continue
|
2013-06-01 12:36:42 +02:00
|
|
|
_attr = '_%s' % attr
|
|
|
|
data = getattr(self, _attr, None)
|
2013-06-02 22:23:58 +02:00
|
|
|
if data is None and last_version is not None:
|
2013-06-01 12:36:42 +02:00
|
|
|
data = getattr(last_version, attr)
|
|
|
|
if data is not None:
|
|
|
|
setattr(new_version, attr, data)
|
|
|
|
return new_version
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-06-01 12:36:42 +02:00
|
|
|
def get_active_version(self):
|
2013-01-26 12:28:51 +01:00
|
|
|
"""
|
2013-06-01 12:36:42 +02:00
|
|
|
Returns the active version of the motion.
|
2012-02-14 16:31:21 +01:00
|
|
|
|
2013-06-02 22:23:58 +02:00
|
|
|
If no active version is set by now, the last_version is used.
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2013-06-01 12:36:42 +02:00
|
|
|
if self.active_version:
|
|
|
|
return self.active_version
|
2013-01-06 12:07:37 +01:00
|
|
|
else:
|
2013-06-01 12:36:42 +02:00
|
|
|
return self.get_last_version()
|
2012-02-15 12:36:50 +01:00
|
|
|
|
2013-06-01 12:36:42 +02:00
|
|
|
def get_last_version(self):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Return the newest version of the motion.
|
|
|
|
"""
|
2013-01-06 12:07:37 +01:00
|
|
|
try:
|
2013-02-03 13:24:29 +01:00
|
|
|
return self.versions.order_by('-version_number')[0]
|
2013-01-06 12:07:37 +01:00
|
|
|
except IndexError:
|
2013-06-01 12:36:42 +02:00
|
|
|
return self.get_new_version()
|
2012-02-15 13:44:55 +01:00
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
def is_submitter(self, user):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2015-04-30 19:13:28 +02:00
|
|
|
Returns True if user is a submitter of this motion, else False.
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2015-04-30 19:13:28 +02:00
|
|
|
return user in self.submitters.all()
|
2013-01-26 18:18:02 +01:00
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
def is_supporter(self, user):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2015-04-30 19:13:28 +02:00
|
|
|
Returns True if user is a supporter of this motion, else False.
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2015-04-30 19:13:28 +02:00
|
|
|
return user in self.supporters.all()
|
2013-05-31 18:30:03 +02:00
|
|
|
|
2013-02-01 12:51:54 +01:00
|
|
|
def create_poll(self):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Create a new poll for this motion.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
|
|
|
Return the new poll object.
|
2013-02-01 16:33:45 +01:00
|
|
|
"""
|
2013-02-06 23:56:21 +01:00
|
|
|
if self.state.allow_create_poll:
|
2015-09-04 20:22:46 +02:00
|
|
|
poll = MotionPoll.objects.create(motion=self)
|
2013-02-01 16:44:06 +01:00
|
|
|
poll.set_options()
|
|
|
|
return poll
|
|
|
|
else:
|
2013-02-06 23:56:21 +01:00
|
|
|
raise WorkflowError('You can not create a poll in state %s.' % self.state.name)
|
2013-02-01 16:33:45 +01:00
|
|
|
|
2015-04-30 19:13:28 +02:00
|
|
|
@property
|
|
|
|
def workflow(self):
|
|
|
|
"""
|
|
|
|
Returns the id of the workflow of the motion.
|
|
|
|
"""
|
2015-11-03 10:03:44 +01:00
|
|
|
# TODO: Rename to workflow_id
|
2015-04-30 19:13:28 +02:00
|
|
|
return self.state.workflow.pk
|
|
|
|
|
2013-03-12 22:03:56 +01:00
|
|
|
def set_state(self, state):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Set the state of the motion.
|
2013-03-12 22:03:56 +01:00
|
|
|
|
2013-06-14 09:56:16 +02:00
|
|
|
'state' can be the id of a state object or a state object.
|
2013-03-12 22:03:56 +01:00
|
|
|
"""
|
|
|
|
if type(state) is int:
|
|
|
|
state = State.objects.get(pk=state)
|
|
|
|
|
2013-03-12 22:13:17 +01:00
|
|
|
if not state.dont_set_identifier:
|
2013-03-12 22:03:56 +01:00
|
|
|
self.set_identifier()
|
|
|
|
self.state = state
|
|
|
|
|
2013-05-13 23:08:55 +02:00
|
|
|
def reset_state(self, workflow=None):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Set the state to the default state.
|
|
|
|
|
2013-06-14 09:56:16 +02:00
|
|
|
'workflow' can be a workflow, an id of a workflow or None.
|
|
|
|
|
2013-05-13 23:08:55 +02:00
|
|
|
If the motion is new and workflow is None, it chooses the default
|
|
|
|
workflow from config.
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2013-06-14 09:56:16 +02:00
|
|
|
if type(workflow) is int:
|
|
|
|
workflow = Workflow.objects.get(pk=workflow)
|
|
|
|
|
|
|
|
if workflow is not None:
|
2013-05-13 23:08:55 +02:00
|
|
|
new_state = workflow.first_state
|
|
|
|
elif self.state:
|
2013-05-09 23:51:58 +02:00
|
|
|
new_state = self.state.workflow.first_state
|
2013-02-06 23:56:21 +01:00
|
|
|
else:
|
2015-06-16 18:12:59 +02:00
|
|
|
new_state = (Workflow.objects.get(pk=config['motions_workflow']).first_state or
|
2015-11-03 10:03:44 +01:00
|
|
|
Workflow.objects.get(pk=config['motions_workflow']).states.all()[0])
|
2013-05-09 23:51:58 +02:00
|
|
|
self.set_state(new_state)
|
2013-02-01 17:20:11 +01:00
|
|
|
|
2016-09-03 21:43:11 +02:00
|
|
|
def set_recommendation(self, recommendation):
|
|
|
|
"""
|
|
|
|
Set the recommendation of the motion.
|
|
|
|
|
|
|
|
'recommendation' can be the id of a state object or a state object.
|
|
|
|
"""
|
|
|
|
if type(recommendation) is int:
|
|
|
|
recommendation = State.objects.get(pk=recommendation)
|
|
|
|
self.recommendation = recommendation
|
|
|
|
|
2016-10-14 21:48:02 +02:00
|
|
|
def follow_recommendation(self):
|
|
|
|
"""
|
|
|
|
Set the state of this motion to its recommendation.
|
|
|
|
"""
|
|
|
|
if self.recommendation is not None:
|
|
|
|
self.set_state(self.recommendation)
|
|
|
|
|
2013-02-02 00:51:08 +01:00
|
|
|
def get_agenda_title(self):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2016-01-25 21:22:22 +01:00
|
|
|
Return a simple title string for the agenda.
|
|
|
|
|
2016-01-27 20:03:53 +01:00
|
|
|
Returns only the motion title so that you have only agenda item number
|
|
|
|
and title in the agenda.
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
2014-08-16 09:25:18 +02:00
|
|
|
return str(self)
|
2013-02-02 00:51:08 +01:00
|
|
|
|
2016-01-25 21:22:22 +01:00
|
|
|
def get_agenda_list_view_title(self):
|
|
|
|
"""
|
|
|
|
Return a title string for the agenda list view.
|
|
|
|
|
2016-01-27 20:03:53 +01:00
|
|
|
Returns only the motion title so that you have agenda item number,
|
|
|
|
title and motion identifier in the agenda.
|
2016-01-25 21:22:22 +01:00
|
|
|
Note: It has to be the same return value like in JavaScript.
|
|
|
|
"""
|
|
|
|
if self.identifier:
|
|
|
|
string = '%s (%s %s)' % (self.title, _(self._meta.verbose_name), self.identifier)
|
|
|
|
else:
|
|
|
|
string = '%s (%s)' % (self.title, _(self._meta.verbose_name))
|
|
|
|
return string
|
|
|
|
|
2015-10-24 19:02:43 +02:00
|
|
|
@property
|
|
|
|
def agenda_item(self):
|
|
|
|
"""
|
|
|
|
Returns the related agenda item.
|
|
|
|
"""
|
2016-09-30 20:42:58 +02:00
|
|
|
# We support only one agenda item so just return the first element of
|
|
|
|
# the queryset.
|
2016-09-18 16:00:31 +02:00
|
|
|
return self.agenda_items.all()[0]
|
2015-10-24 19:02:43 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def agenda_item_id(self):
|
2013-05-24 00:31:52 +02:00
|
|
|
"""
|
2015-10-24 19:02:43 +02:00
|
|
|
Returns the id of the agenda item object related to this object.
|
2013-05-24 00:31:52 +02:00
|
|
|
"""
|
2015-10-24 19:02:43 +02:00
|
|
|
return self.agenda_item.pk
|
2013-02-02 00:51:08 +01:00
|
|
|
|
2013-02-02 10:24:28 +01:00
|
|
|
def get_allowed_actions(self, person):
|
2013-04-19 14:12:49 +02:00
|
|
|
"""
|
|
|
|
Return a dictonary with all allowed actions for a specific person.
|
2013-02-02 10:24:28 +01:00
|
|
|
|
|
|
|
The dictonary contains the following actions.
|
|
|
|
|
2014-12-20 20:13:30 +01:00
|
|
|
* see
|
2013-05-31 18:30:03 +02:00
|
|
|
* update / edit
|
2013-02-02 10:24:28 +01:00
|
|
|
* delete
|
|
|
|
* create_poll
|
|
|
|
* support
|
|
|
|
* unsupport
|
|
|
|
* change_state
|
|
|
|
* reset_state
|
2016-09-03 21:43:11 +02:00
|
|
|
* change_recommendation
|
2015-11-19 20:51:39 +01:00
|
|
|
|
|
|
|
NOTE: If you update this function please also update the
|
|
|
|
'isAllowed' function on client side in motions/site.js.
|
2013-02-02 10:24:28 +01:00
|
|
|
"""
|
2015-04-30 19:13:28 +02:00
|
|
|
# TODO: Remove this method and implement these things in the views.
|
2013-02-02 10:24:28 +01:00
|
|
|
actions = {
|
2015-03-26 05:36:10 +01:00
|
|
|
'see': (person.has_perm('motions.can_see') and
|
2014-12-20 20:13:30 +01:00
|
|
|
(not self.state.required_permission_to_see or
|
|
|
|
person.has_perm(self.state.required_permission_to_see) or
|
|
|
|
self.is_submitter(person))),
|
|
|
|
|
2015-03-26 05:36:10 +01:00
|
|
|
'update': (person.has_perm('motions.can_manage') or
|
2014-12-20 20:13:30 +01:00
|
|
|
(self.is_submitter(person) and
|
|
|
|
self.state.allow_submitter_edit)),
|
2013-05-31 18:30:03 +02:00
|
|
|
|
2015-03-26 05:36:10 +01:00
|
|
|
'delete': person.has_perm('motions.can_manage'),
|
2013-02-02 10:24:28 +01:00
|
|
|
|
2015-03-26 05:36:10 +01:00
|
|
|
'create_poll': (person.has_perm('motions.can_manage') and
|
2013-02-06 23:56:21 +01:00
|
|
|
self.state.allow_create_poll),
|
2013-02-02 10:24:28 +01:00
|
|
|
|
2013-02-06 23:56:21 +01:00
|
|
|
'support': (self.state.allow_support and
|
2015-06-16 18:12:59 +02:00
|
|
|
config['motions_min_supporters'] > 0 and
|
2013-03-09 14:29:40 +01:00
|
|
|
not self.is_submitter(person) and
|
|
|
|
not self.is_supporter(person)),
|
|
|
|
|
|
|
|
'unsupport': (self.state.allow_support and
|
|
|
|
self.is_supporter(person)),
|
2013-02-02 10:24:28 +01:00
|
|
|
|
2015-03-26 05:36:10 +01:00
|
|
|
'change_state': person.has_perm('motions.can_manage'),
|
2013-02-02 10:24:28 +01:00
|
|
|
|
2016-09-03 21:43:11 +02:00
|
|
|
'reset_state': person.has_perm('motions.can_manage'),
|
|
|
|
|
|
|
|
'change_recommendation': person.has_perm('motions.can_manage'),
|
|
|
|
|
|
|
|
}
|
2013-05-31 18:30:03 +02:00
|
|
|
|
|
|
|
actions['edit'] = actions['update']
|
|
|
|
|
2013-02-02 10:24:28 +01:00
|
|
|
return actions
|
|
|
|
|
2016-10-14 21:48:02 +02:00
|
|
|
def write_log(self, message_list, person=None, skip_autoupdate=False):
|
2013-05-16 00:58:37 +02:00
|
|
|
"""
|
|
|
|
Write a log message.
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-05-16 00:58:37 +02:00
|
|
|
The message should be in English and translatable,
|
2013-05-16 01:14:14 +02:00
|
|
|
e. g. motion.write_log(message_list=[ugettext_noop('Message Text')])
|
2013-02-05 18:46:46 +01:00
|
|
|
"""
|
2016-09-08 09:44:47 +02:00
|
|
|
if person and not person.is_authenticated():
|
|
|
|
person = None
|
2016-10-14 21:48:02 +02:00
|
|
|
motion_log = MotionLog(motion=self, message_list=message_list, person=person)
|
|
|
|
motion_log.save(skip_autoupdate=skip_autoupdate)
|
2013-02-02 21:40:23 +01:00
|
|
|
|
2014-12-25 10:58:52 +01:00
|
|
|
def is_amendment(self):
|
|
|
|
"""
|
|
|
|
Returns True if the motion is an amendment.
|
|
|
|
|
|
|
|
A motion is a amendment if amendments are activated in the config and
|
|
|
|
the motion has a parent.
|
|
|
|
"""
|
2015-06-16 18:12:59 +02:00
|
|
|
return config['motions_amendments_enabled'] and self.parent is not None
|
2014-12-25 10:58:52 +01:00
|
|
|
|
2016-01-03 15:33:51 +01:00
|
|
|
def get_search_index_string(self):
|
|
|
|
"""
|
|
|
|
Returns a string that can be indexed for the search.
|
|
|
|
"""
|
|
|
|
return " ".join((
|
|
|
|
self.title or '',
|
|
|
|
self.text or '',
|
|
|
|
self.reason or '',
|
|
|
|
str(self.category) if self.category else '',
|
|
|
|
user_name_helper(self.submitters.all()),
|
|
|
|
user_name_helper(self.supporters.all()),
|
|
|
|
" ".join(tag.name for tag in self.tags.all())))
|
|
|
|
|
2012-02-15 13:44:55 +01:00
|
|
|
|
2015-06-16 10:37:23 +02:00
|
|
|
class MotionVersion(RESTModelMixin, models.Model):
|
2013-02-05 18:46:46 +01:00
|
|
|
"""
|
2013-04-19 14:12:49 +02:00
|
|
|
A MotionVersion object saves some date of the motion.
|
|
|
|
"""
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
motion = models.ForeignKey(
|
|
|
|
Motion,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name='versions')
|
2013-02-06 23:56:21 +01:00
|
|
|
"""The motion to which the version belongs."""
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-02-03 13:24:29 +01:00
|
|
|
version_number = models.PositiveIntegerField(default=1)
|
2013-02-05 18:46:46 +01:00
|
|
|
"""An id for this version in realation to a motion.
|
|
|
|
|
|
|
|
Is unique for each motion.
|
|
|
|
"""
|
|
|
|
|
2016-01-09 13:32:56 +01:00
|
|
|
title = models.CharField(max_length=255)
|
2013-02-06 23:56:21 +01:00
|
|
|
"""The title of a motion."""
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2016-01-09 13:32:56 +01:00
|
|
|
text = models.TextField()
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The text of a motion."""
|
|
|
|
|
2016-01-09 13:32:56 +01:00
|
|
|
reason = models.TextField(null=True, blank=True)
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The reason for a motion."""
|
|
|
|
|
2013-01-06 12:07:37 +01:00
|
|
|
creation_time = models.DateTimeField(auto_now=True)
|
2013-02-06 23:56:21 +01:00
|
|
|
"""Time when the version was saved."""
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-02-03 13:24:29 +01:00
|
|
|
class Meta:
|
2015-12-10 00:20:59 +01:00
|
|
|
default_permissions = ()
|
2013-02-03 13:24:29 +01:00
|
|
|
unique_together = ("motion", "version_number")
|
|
|
|
|
2014-08-16 09:25:18 +02:00
|
|
|
def __str__(self):
|
2013-02-05 18:46:46 +01:00
|
|
|
"""Return a string, representing this object."""
|
2013-04-22 19:59:05 +02:00
|
|
|
counter = self.version_number or ugettext_lazy('new')
|
2013-06-13 15:43:17 +02:00
|
|
|
return "Motion %s, Version %s" % (self.motion_id, counter)
|
2013-01-06 12:07:37 +01:00
|
|
|
|
2013-01-26 15:25:54 +01:00
|
|
|
@property
|
2013-02-03 13:24:29 +01:00
|
|
|
def active(self):
|
2013-02-05 18:46:46 +01:00
|
|
|
"""Return True, if the version is the active version of a motion. Else: False."""
|
2013-02-03 13:24:29 +01:00
|
|
|
return self.active_version.exists()
|
2012-04-12 16:21:30 +02:00
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
def get_root_rest_element(self):
|
|
|
|
"""
|
|
|
|
Returns the motion to this instance which is the root REST element.
|
|
|
|
"""
|
|
|
|
return self.motion
|
|
|
|
|
2012-04-14 12:52:56 +02:00
|
|
|
|
2016-09-10 18:49:38 +02:00
|
|
|
class MotionChangeRecommendationManager(models.Manager):
|
|
|
|
"""
|
|
|
|
Customized model manager to support our get_full_queryset method.
|
|
|
|
"""
|
|
|
|
def get_full_queryset(self):
|
|
|
|
"""
|
|
|
|
Returns the normal queryset with all change recommendations. In the background we
|
|
|
|
join and prefetch all related models.
|
|
|
|
"""
|
|
|
|
return self.get_queryset()
|
|
|
|
|
|
|
|
|
|
|
|
class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
|
|
|
"""
|
|
|
|
A MotionChangeRecommendation object saves change recommendations for a specific MotionVersion
|
|
|
|
"""
|
|
|
|
|
|
|
|
access_permissions = MotionChangeRecommendationAccessPermissions()
|
|
|
|
|
|
|
|
objects = MotionChangeRecommendationManager()
|
|
|
|
|
|
|
|
motion_version = models.ForeignKey(
|
|
|
|
MotionVersion,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name='change_recommendations')
|
|
|
|
"""The motion version to which the change recommendation belongs."""
|
|
|
|
|
2016-11-13 13:19:16 +01:00
|
|
|
rejected = models.BooleanField(default=False)
|
|
|
|
"""If true, this change recommendation has been rejected"""
|
2016-09-10 18:49:38 +02:00
|
|
|
|
2016-11-13 16:42:56 +01:00
|
|
|
type = models.PositiveIntegerField(default=0)
|
|
|
|
"""Replacement (0), Insertion (1), Deletion (2)"""
|
|
|
|
|
2016-09-10 18:49:38 +02:00
|
|
|
line_from = models.PositiveIntegerField()
|
|
|
|
"""The number or the first affected line"""
|
|
|
|
|
|
|
|
line_to = models.PositiveIntegerField()
|
|
|
|
"""The number or the last affected line (inclusive)"""
|
|
|
|
|
|
|
|
text = models.TextField(blank=True)
|
|
|
|
"""The replacement for the section of the original text specified by version, line_from and line_to"""
|
|
|
|
|
|
|
|
author = models.ForeignKey(
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
null=True)
|
|
|
|
"""A user object, who created this change recommendation. Optional."""
|
|
|
|
|
|
|
|
creation_time = models.DateTimeField(auto_now=True)
|
|
|
|
"""Time when the change recommendation was saved."""
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
default_permissions = ()
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
"""Return a string, representing this object."""
|
|
|
|
return "Recommendation for Version %s, line %s - %s" % (self.motion_version_id, self.line_from, self.line_to)
|
|
|
|
|
|
|
|
def get_root_rest_element(self):
|
|
|
|
"""
|
|
|
|
Returns this instance, which is the root REST element.
|
|
|
|
"""
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
2015-06-16 10:37:23 +02:00
|
|
|
class Category(RESTModelMixin, models.Model):
|
2016-02-11 22:58:32 +01:00
|
|
|
"""
|
|
|
|
Model for categories of motions.
|
|
|
|
"""
|
|
|
|
access_permissions = CategoryAccessPermissions()
|
|
|
|
|
2016-01-09 13:32:56 +01:00
|
|
|
name = models.CharField(max_length=255)
|
2013-03-12 23:54:37 +01:00
|
|
|
"""Name of the category."""
|
|
|
|
|
2016-01-09 13:32:56 +01:00
|
|
|
prefix = models.CharField(blank=True, max_length=32)
|
2013-03-12 23:54:37 +01:00
|
|
|
"""Prefix of the category.
|
|
|
|
|
|
|
|
Used to build the identifier of a motion.
|
|
|
|
"""
|
2013-03-11 20:17:19 +01:00
|
|
|
|
2013-03-11 21:29:56 +01:00
|
|
|
class Meta:
|
2015-12-10 00:20:59 +01:00
|
|
|
default_permissions = ()
|
2013-03-11 21:29:56 +01:00
|
|
|
ordering = ['prefix']
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2015-12-10 00:20:59 +01:00
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
2013-02-01 12:51:54 +01:00
|
|
|
|
2016-10-01 20:42:44 +02:00
|
|
|
class MotionBlockManager(models.Manager):
|
|
|
|
"""
|
|
|
|
Customized model manager to support our get_full_queryset method.
|
|
|
|
"""
|
|
|
|
def get_full_queryset(self):
|
|
|
|
"""
|
|
|
|
Returns the normal queryset with all motion blocks. In the
|
|
|
|
background the related agenda item is prefetched from the database.
|
|
|
|
"""
|
|
|
|
return self.get_queryset().prefetch_related('agenda_items')
|
|
|
|
|
|
|
|
|
|
|
|
class MotionBlock(RESTModelMixin, models.Model):
|
|
|
|
"""
|
|
|
|
Model for blocks of motions.
|
|
|
|
"""
|
|
|
|
access_permissions = MotionBlockAccessPermissions()
|
|
|
|
|
|
|
|
objects = MotionBlockManager()
|
|
|
|
|
|
|
|
title = models.CharField(max_length=255)
|
|
|
|
|
|
|
|
# In theory there could be one then more agenda_item. But we support only
|
|
|
|
# one. See the property agenda_item.
|
|
|
|
agenda_items = GenericRelation(Item, related_name='topics')
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
default_permissions = ()
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.title
|
|
|
|
|
|
|
|
@property
|
|
|
|
def agenda_item(self):
|
|
|
|
"""
|
|
|
|
Returns the related agenda item.
|
|
|
|
"""
|
|
|
|
# We support only one agenda item so just return the first element of
|
|
|
|
# the queryset.
|
|
|
|
return self.agenda_items.all()[0]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def agenda_item_id(self):
|
|
|
|
"""
|
|
|
|
Returns the id of the agenda item object related to this object.
|
|
|
|
"""
|
|
|
|
return self.agenda_item.pk
|
|
|
|
|
|
|
|
def get_agenda_title(self):
|
|
|
|
return self.title
|
|
|
|
|
|
|
|
def get_agenda_list_view_title(self):
|
|
|
|
return self.title
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_collection_string(cls):
|
|
|
|
# TODO: Fix generic method to support camelCase, #2480.
|
|
|
|
return 'motions/motion-block'
|
|
|
|
|
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
class MotionLog(RESTModelMixin, models.Model):
|
2013-02-05 18:46:46 +01:00
|
|
|
"""Save a logmessage for a motion."""
|
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
motion = models.ForeignKey(
|
|
|
|
Motion,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name='log_messages')
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The motion to witch the object belongs."""
|
|
|
|
|
2013-05-16 01:14:14 +02:00
|
|
|
message_list = JSONField()
|
|
|
|
"""
|
2015-01-24 16:35:50 +01:00
|
|
|
The log message. It should be a list of strings in English.
|
2013-02-05 18:46:46 +01:00
|
|
|
"""
|
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
person = models.ForeignKey(
|
|
|
|
settings.AUTH_USER_MODEL,
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
null=True)
|
2015-09-07 17:09:29 +02:00
|
|
|
"""A user object, who created the log message. Optional."""
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2013-02-02 21:40:23 +01:00
|
|
|
time = models.DateTimeField(auto_now=True)
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The Time, when the loged action was performed."""
|
2013-02-02 21:40:23 +01:00
|
|
|
|
|
|
|
class Meta:
|
2015-12-10 00:20:59 +01:00
|
|
|
default_permissions = ()
|
2013-02-02 21:40:23 +01:00
|
|
|
ordering = ['-time']
|
|
|
|
|
2014-08-16 09:25:18 +02:00
|
|
|
def __str__(self):
|
2013-05-16 01:14:14 +02:00
|
|
|
"""
|
|
|
|
Return a string, representing the log message.
|
|
|
|
"""
|
2013-03-09 14:29:40 +01:00
|
|
|
time = formats.date_format(self.time, 'DATETIME_FORMAT')
|
2013-06-19 21:40:59 +02:00
|
|
|
time_and_messages = '%s ' % time + ''.join(map(_, self.message_list))
|
2013-05-16 01:14:14 +02:00
|
|
|
if self.person is not None:
|
2013-06-19 21:40:59 +02:00
|
|
|
return _('%(time_and_messages)s by %(person)s') % {'time_and_messages': time_and_messages,
|
|
|
|
'person': self.person}
|
|
|
|
return time_and_messages
|
2013-02-02 21:40:23 +01:00
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
def get_root_rest_element(self):
|
|
|
|
"""
|
|
|
|
Returns the motion to this instance which is the root REST element.
|
|
|
|
"""
|
|
|
|
return self.motion
|
|
|
|
|
2013-02-03 14:14:07 +01:00
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
class MotionVote(RESTModelMixin, BaseVote):
|
2013-02-05 18:46:46 +01:00
|
|
|
"""Saves the votes for a MotionPoll.
|
|
|
|
|
|
|
|
There should allways be three MotionVote objects for each poll,
|
|
|
|
one for 'yes', 'no', and 'abstain'."""
|
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
option = models.ForeignKey(
|
|
|
|
'MotionOption',
|
|
|
|
on_delete=models.CASCADE)
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The option object, to witch the vote belongs."""
|
2013-02-01 12:51:54 +01:00
|
|
|
|
2015-12-10 00:20:59 +01:00
|
|
|
class Meta:
|
|
|
|
default_permissions = ()
|
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
def get_root_rest_element(self):
|
|
|
|
"""
|
|
|
|
Returns the motion to this instance which is the root REST element.
|
|
|
|
"""
|
|
|
|
return self.option.poll.motion
|
|
|
|
|
2013-02-01 12:51:54 +01:00
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
class MotionOption(RESTModelMixin, BaseOption):
|
2013-02-05 18:46:46 +01:00
|
|
|
"""Links between the MotionPollClass and the MotionVoteClass.
|
|
|
|
|
|
|
|
There should be one MotionOption object for each poll."""
|
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
poll = models.ForeignKey(
|
|
|
|
'MotionPoll',
|
|
|
|
on_delete=models.CASCADE)
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The poll object, to witch the object belongs."""
|
|
|
|
|
2013-02-01 12:51:54 +01:00
|
|
|
vote_class = MotionVote
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The VoteClass, to witch this Class links."""
|
2013-02-01 12:51:54 +01:00
|
|
|
|
2015-12-10 00:20:59 +01:00
|
|
|
class Meta:
|
|
|
|
default_permissions = ()
|
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
def get_root_rest_element(self):
|
|
|
|
"""
|
|
|
|
Returns the motion to this instance which is the root REST element.
|
|
|
|
"""
|
|
|
|
return self.poll.motion
|
|
|
|
|
2013-02-01 12:51:54 +01:00
|
|
|
|
2015-06-29 13:31:07 +02:00
|
|
|
class MotionPoll(RESTModelMixin, CollectDefaultVotesMixin, BasePoll):
|
2013-10-17 11:34:54 +02:00
|
|
|
"""The Class to saves the vote result for a motion poll."""
|
2013-02-05 18:46:46 +01:00
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
motion = models.ForeignKey(
|
|
|
|
Motion,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name='polls')
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The motion to witch the object belongs."""
|
|
|
|
|
2013-02-01 12:51:54 +01:00
|
|
|
option_class = MotionOption
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The option class, witch links between this object the the votes."""
|
|
|
|
|
2015-12-07 12:40:30 +01:00
|
|
|
vote_values = ['Yes', 'No', 'Abstain']
|
2013-02-05 18:46:46 +01:00
|
|
|
"""The possible anwers for the poll. 'Yes, 'No' and 'Abstain'."""
|
2013-02-01 12:51:54 +01:00
|
|
|
|
2015-12-10 00:20:59 +01:00
|
|
|
class Meta:
|
|
|
|
default_permissions = ()
|
|
|
|
|
2014-08-16 09:25:18 +02:00
|
|
|
def __str__(self):
|
2015-09-04 20:22:46 +02:00
|
|
|
"""
|
|
|
|
Representation method only for debugging purposes.
|
|
|
|
"""
|
|
|
|
return 'MotionPoll for motion %s' % self.motion
|
2013-02-01 13:24:44 +01:00
|
|
|
|
2013-02-01 12:51:54 +01:00
|
|
|
def set_options(self):
|
2013-02-05 18:46:46 +01:00
|
|
|
"""Create the option class for this poll."""
|
2014-03-27 20:38:13 +01:00
|
|
|
# TODO: maybe it is possible with .create() to call this without poll=self
|
|
|
|
# or call this in save()
|
2013-02-01 12:51:54 +01:00
|
|
|
self.get_option_class()(poll=self).save()
|
|
|
|
|
2014-04-10 20:18:22 +02:00
|
|
|
def get_percent_base_choice(self):
|
2015-06-16 18:12:59 +02:00
|
|
|
return config['motions_poll_100_percent_base']
|
2014-04-10 20:18:22 +02:00
|
|
|
|
2013-10-17 11:34:54 +02:00
|
|
|
def get_slide_context(self, **context):
|
|
|
|
return super(MotionPoll, self).get_slide_context(poll=self)
|
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
def get_root_rest_element(self):
|
|
|
|
"""
|
|
|
|
Returns the motion to this instance which is the root REST element.
|
|
|
|
"""
|
|
|
|
return self.motion
|
|
|
|
|
2013-02-06 23:56:21 +01:00
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
class State(RESTModelMixin, models.Model):
|
2013-10-03 21:49:51 +02:00
|
|
|
"""
|
|
|
|
Defines a state for a motion.
|
2013-02-06 23:56:21 +01:00
|
|
|
|
2016-09-03 21:43:11 +02:00
|
|
|
Every state belongs to a workflow. All states of a workflow are linked
|
|
|
|
together via 'next_states'. One of these states is the first state, but
|
|
|
|
this is saved in the workflow table (one-to-one relation). In every
|
|
|
|
state you can configure some handling of a motion. See the following
|
|
|
|
fields for more information.
|
|
|
|
|
|
|
|
Additionally every motion can refer to one state as recommendation of
|
|
|
|
an person or committee (see config 'motions_recommendations_by'). This
|
|
|
|
means that the person or committee recommends to set the motion to this
|
|
|
|
state.
|
2013-02-06 23:56:21 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
name = models.CharField(max_length=255)
|
|
|
|
"""A string representing the state."""
|
|
|
|
|
|
|
|
action_word = models.CharField(max_length=255)
|
|
|
|
"""An alternative string to be used for a button to switch to this state."""
|
|
|
|
|
2016-09-03 21:43:11 +02:00
|
|
|
recommendation_label = models.CharField(max_length=255, null=True)
|
|
|
|
"""A string for a recommendation to set the motion to this state."""
|
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
workflow = models.ForeignKey(
|
|
|
|
'Workflow',
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name='states')
|
2013-02-06 23:56:21 +01:00
|
|
|
"""A many-to-one relation to a workflow."""
|
|
|
|
|
|
|
|
next_states = models.ManyToManyField('self', symmetrical=False)
|
|
|
|
"""A many-to-many relation to all states, that can be choosen from this state."""
|
|
|
|
|
2015-11-03 21:38:53 +01:00
|
|
|
css_class = models.CharField(max_length=255, default='primary')
|
|
|
|
"""
|
|
|
|
A css class string for showing the state name in a coloured label based on bootstrap,
|
|
|
|
e.g. 'danger' (red), 'success' (green), 'warning' (yellow), 'default' (grey).
|
|
|
|
Default value is 'primary' (blue).
|
|
|
|
"""
|
2013-02-06 23:56:21 +01:00
|
|
|
|
2014-12-20 20:13:30 +01:00
|
|
|
required_permission_to_see = models.CharField(max_length=255, blank=True)
|
|
|
|
"""
|
|
|
|
A permission string. If not empty, the user has to have this permission to
|
|
|
|
see a motion in this state.
|
|
|
|
|
|
|
|
To use this feature change the database entry of a state object and add
|
|
|
|
your favourite permission string. You can do this e. g. by editing the
|
2015-03-26 05:36:10 +01:00
|
|
|
definitions in create_builtin_workflows() in openslides/motions/signals.py.
|
2014-12-20 20:13:30 +01:00
|
|
|
"""
|
|
|
|
|
2013-02-06 23:56:21 +01:00
|
|
|
allow_support = models.BooleanField(default=False)
|
|
|
|
"""If true, persons can support the motion in this state."""
|
|
|
|
|
|
|
|
allow_create_poll = models.BooleanField(default=False)
|
|
|
|
"""If true, polls can be created in this state."""
|
|
|
|
|
|
|
|
allow_submitter_edit = models.BooleanField(default=False)
|
|
|
|
"""If true, the submitter can edit the motion in this state."""
|
|
|
|
|
|
|
|
versioning = models.BooleanField(default=False)
|
|
|
|
"""
|
|
|
|
If true, editing the motion will create a new version by default.
|
|
|
|
This behavior can be changed by the form and view, e. g. via the
|
|
|
|
MotionDisableVersioningMixin.
|
|
|
|
"""
|
|
|
|
|
2013-04-22 22:31:58 +02:00
|
|
|
leave_old_version_active = models.BooleanField(default=False)
|
2013-02-06 23:56:21 +01:00
|
|
|
"""If true, new versions are not automaticly set active."""
|
|
|
|
|
2013-03-12 22:13:17 +01:00
|
|
|
dont_set_identifier = models.BooleanField(default=False)
|
2014-12-25 10:58:52 +01:00
|
|
|
"""
|
|
|
|
Decides if the motion gets an identifier.
|
2013-03-12 22:13:17 +01:00
|
|
|
|
|
|
|
If true, the motion does not get an identifier if the state change to
|
2014-12-25 10:58:52 +01:00
|
|
|
this one, else it does.
|
|
|
|
"""
|
2013-03-12 22:03:56 +01:00
|
|
|
|
2016-10-27 14:01:12 +02:00
|
|
|
show_state_extension_field = models.BooleanField(default=False)
|
|
|
|
"""
|
|
|
|
If true, an additional input field (from motion comment) is visible
|
|
|
|
to extend the state name. The full state name is composed of the given
|
|
|
|
state name and the entered value of this input field.
|
|
|
|
"""
|
|
|
|
|
|
|
|
show_recommendation_extension_field = models.BooleanField(default=False)
|
|
|
|
"""
|
|
|
|
If true, an additional input field (from motion comment) is visible
|
|
|
|
to extend the recommendation label. The full recommendation string is
|
|
|
|
composed of the given recommendation label and the entered value of this input field.
|
|
|
|
"""
|
|
|
|
|
2015-12-10 00:20:59 +01:00
|
|
|
class Meta:
|
|
|
|
default_permissions = ()
|
|
|
|
|
2014-08-16 09:25:18 +02:00
|
|
|
def __str__(self):
|
2013-02-06 23:56:21 +01:00
|
|
|
"""Returns the name of the state."""
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
def save(self, **kwargs):
|
2013-06-02 22:23:58 +02:00
|
|
|
"""Saves a state in the database.
|
2013-02-06 23:56:21 +01:00
|
|
|
|
2016-09-03 21:43:11 +02:00
|
|
|
Used to check the integrity before saving. Also used to check that
|
|
|
|
recommendation_label is not an empty string.
|
2013-02-06 23:56:21 +01:00
|
|
|
"""
|
|
|
|
self.check_next_states()
|
2016-09-03 21:43:11 +02:00
|
|
|
if self.recommendation_label == '':
|
|
|
|
raise WorkflowError('The field recommendation_label of {} must not '
|
|
|
|
'be an empty string.'.format(self))
|
2013-02-06 23:56:21 +01:00
|
|
|
super(State, self).save(**kwargs)
|
|
|
|
|
|
|
|
def get_action_word(self):
|
|
|
|
"""Returns the alternative name of the state if it exists."""
|
|
|
|
return self.action_word or self.name
|
|
|
|
|
|
|
|
def check_next_states(self):
|
|
|
|
"""Checks whether all next states of a state belong to the correct workflow."""
|
|
|
|
# No check if it is a new state which has not been saved yet.
|
|
|
|
if not self.id:
|
|
|
|
return
|
|
|
|
for state in self.next_states.all():
|
|
|
|
if not state.workflow == self.workflow:
|
|
|
|
raise WorkflowError('%s can not be next state of %s because it does not belong to the same workflow.' % (state, self))
|
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
def get_root_rest_element(self):
|
|
|
|
"""
|
|
|
|
Returns the workflow to this instance which is the root REST element.
|
|
|
|
"""
|
|
|
|
return self.workflow
|
|
|
|
|
2013-02-06 23:56:21 +01:00
|
|
|
|
2016-09-18 16:00:31 +02:00
|
|
|
class WorkflowManager(models.Manager):
|
2016-09-30 20:42:58 +02:00
|
|
|
"""
|
|
|
|
Customized model manager to support our get_full_queryset method.
|
|
|
|
"""
|
2016-09-18 16:00:31 +02:00
|
|
|
def get_full_queryset(self):
|
2016-09-30 20:42:58 +02:00
|
|
|
"""
|
|
|
|
Returns the normal queryset with all workflows. In the background
|
|
|
|
the first state is joined and all states and next states are
|
|
|
|
prefetched from the database.
|
|
|
|
"""
|
2016-09-18 16:00:31 +02:00
|
|
|
return (self.get_queryset()
|
|
|
|
.select_related('first_state')
|
|
|
|
.prefetch_related('states', 'states__next_states'))
|
|
|
|
|
|
|
|
|
2015-01-24 16:35:50 +01:00
|
|
|
class Workflow(RESTModelMixin, models.Model):
|
2016-02-11 22:58:32 +01:00
|
|
|
"""
|
|
|
|
Defines a workflow for a motion.
|
|
|
|
"""
|
|
|
|
access_permissions = WorkflowAccessPermissions()
|
2013-02-06 23:56:21 +01:00
|
|
|
|
2016-09-18 16:00:31 +02:00
|
|
|
objects = WorkflowManager()
|
|
|
|
|
2013-02-06 23:56:21 +01:00
|
|
|
name = models.CharField(max_length=255)
|
|
|
|
"""A string representing the workflow."""
|
|
|
|
|
2016-01-09 09:58:22 +01:00
|
|
|
first_state = models.OneToOneField(
|
|
|
|
State,
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
related_name='+',
|
|
|
|
null=True)
|
2013-02-06 23:56:21 +01:00
|
|
|
"""A one-to-one relation to a state, the starting point for the workflow."""
|
|
|
|
|
2015-12-10 00:20:59 +01:00
|
|
|
class Meta:
|
|
|
|
default_permissions = ()
|
|
|
|
|
2014-08-16 09:25:18 +02:00
|
|
|
def __str__(self):
|
2013-02-06 23:56:21 +01:00
|
|
|
"""Returns the name of the workflow."""
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
def save(self, **kwargs):
|
2013-06-02 22:23:58 +02:00
|
|
|
"""Saves a workflow in the database.
|
2013-02-06 23:56:21 +01:00
|
|
|
|
|
|
|
Used to check the integrity before saving.
|
|
|
|
"""
|
|
|
|
self.check_first_state()
|
|
|
|
super(Workflow, self).save(**kwargs)
|
|
|
|
|
|
|
|
def check_first_state(self):
|
|
|
|
"""Checks whether the first_state itself belongs to the workflow."""
|
2013-03-11 21:32:09 +01:00
|
|
|
if self.first_state and not self.first_state.workflow == self:
|
2014-10-11 14:34:49 +02:00
|
|
|
raise WorkflowError(
|
|
|
|
'%s can not be first state of %s because it '
|
|
|
|
'does not belong to it.' % (self.first_state, self))
|