Merge pull request #531 from normanjaeckel/workflow_2
Work on workflow system. Now manager can choose the default workflow for a motion.
This commit is contained in:
commit
e4a48cf294
@ -28,10 +28,6 @@ SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
||||
|
||||
ugettext = lambda s: s
|
||||
|
||||
MOTION_WORKFLOW = (
|
||||
('default', ugettext('default'), 'openslides.motion.workflow.default_workflow'),
|
||||
)
|
||||
|
||||
LANGUAGES = (
|
||||
('de', ugettext('German')),
|
||||
('en', ugettext('English')),
|
||||
|
@ -3,7 +3,7 @@
|
||||
openslides.motion
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The OpenSlides motion app appends the functionality to OpenSlides, to
|
||||
The OpenSlides motion app appends the functionality to OpenSlides to
|
||||
manage motions.
|
||||
|
||||
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||
|
23
openslides/motion/exceptions.py
Normal file
23
openslides/motion/exceptions.py
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
openslides.motion.exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Exceptions for the motion app.
|
||||
|
||||
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||
:license: GNU GPL, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from openslides.utils.exceptions import OpenSlidesError
|
||||
|
||||
|
||||
class MotionError(OpenSlidesError):
|
||||
"""Exception raised when errors in the motion accure."""
|
||||
pass
|
||||
|
||||
|
||||
class WorkflowError(OpenSlidesError):
|
||||
"""Exception raised when errors in a workflow or state accure."""
|
||||
pass
|
280
openslides/motion/fixtures/initial_data.json
Normal file
280
openslides/motion/fixtures/initial_data.json
Normal file
@ -0,0 +1,280 @@
|
||||
[
|
||||
{
|
||||
"pk":1,
|
||||
"model":"motion.workflow",
|
||||
"fields":{
|
||||
"name":"Simple Workflow",
|
||||
"first_state":1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":2,
|
||||
"model":"motion.workflow",
|
||||
"fields":{
|
||||
"name":"Complex Workflow",
|
||||
"first_state":5
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":1,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"submitted",
|
||||
"workflow":1,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":true,
|
||||
"next_states":[
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"allow_support":true,
|
||||
"action_word":"",
|
||||
"icon":"",
|
||||
"versioning":false,
|
||||
"allow_create_poll":true
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":2,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"accepted",
|
||||
"workflow":1,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"accept",
|
||||
"icon":"",
|
||||
"versioning":false,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":3,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"rejected",
|
||||
"workflow":1,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"reject",
|
||||
"icon":"",
|
||||
"versioning":false,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":4,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"not decided",
|
||||
"workflow":1,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"do not decide",
|
||||
"icon":"",
|
||||
"versioning":false,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":5,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"published",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":true,
|
||||
"next_states":[
|
||||
6,
|
||||
9,
|
||||
14
|
||||
],
|
||||
"allow_support":true,
|
||||
"action_word":"",
|
||||
"icon":"",
|
||||
"versioning":false,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":6,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"permitted",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":true,
|
||||
"allow_submitter_edit":true,
|
||||
"next_states":[
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"permit",
|
||||
"icon":"",
|
||||
"versioning":true,
|
||||
"allow_create_poll":true
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":7,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"accepted",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"accept",
|
||||
"icon":"",
|
||||
"versioning":true,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":8,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"rejected",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"reject",
|
||||
"icon":"",
|
||||
"versioning":true,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":9,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"withdrawed",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"withdraw",
|
||||
"icon":"",
|
||||
"versioning":true,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":10,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"adjourned",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"adjourn",
|
||||
"icon":"",
|
||||
"versioning":true,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":11,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"not concerned",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"",
|
||||
"icon":"",
|
||||
"versioning":true,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":12,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"commited a bill",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"commit a bill",
|
||||
"icon":"",
|
||||
"versioning":true,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":13,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"needs review",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"",
|
||||
"icon":"",
|
||||
"versioning":true,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk":14,
|
||||
"model":"motion.state",
|
||||
"fields":{
|
||||
"name":"rejected (not authorized)",
|
||||
"workflow":2,
|
||||
"dont_set_new_version_active":false,
|
||||
"allow_submitter_edit":false,
|
||||
"next_states":[
|
||||
|
||||
],
|
||||
"allow_support":false,
|
||||
"action_word":"reject (not authorized)",
|
||||
"icon":"",
|
||||
"versioning":true,
|
||||
"allow_create_poll":false
|
||||
}
|
||||
}
|
||||
]
|
@ -15,28 +15,27 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
from openslides.utils.forms import CssClassMixin
|
||||
from openslides.utils.person import PersonFormField, MultiplePersonFormField
|
||||
from .models import Motion
|
||||
from .workflow import motion_workflow_choices
|
||||
from .models import Motion, Workflow
|
||||
|
||||
|
||||
class BaseMotionForm(forms.ModelForm, CssClassMixin):
|
||||
"""Base FormClass for a Motion.
|
||||
|
||||
For it's own, it append the version data es fields.
|
||||
For it's own, it append the version data to the fields.
|
||||
|
||||
The Class can be mixed with the following Mixins to add fields for the
|
||||
The class can be mixed with the following mixins to add fields for the
|
||||
submitter, supporters etc.
|
||||
"""
|
||||
|
||||
title = forms.CharField(widget=forms.TextInput(), label=_("Title"))
|
||||
"""Title of the Motion. Will be saved in a MotionVersion object."""
|
||||
"""Title of the motion. Will be saved in a MotionVersion object."""
|
||||
|
||||
text = forms.CharField(widget=forms.Textarea(), label=_("Text"))
|
||||
"""Text of the Motion. Will be saved in a MotionVersion object."""
|
||||
"""Text of the motion. Will be saved in a MotionVersion object."""
|
||||
|
||||
reason = forms.CharField(
|
||||
widget=forms.Textarea(), required=False, label=_("Reason"))
|
||||
"""Reason of the Motion. will be saved in a MotionVersion object."""
|
||||
"""Reason of the motion. will be saved in a MotionVersion object."""
|
||||
|
||||
class Meta:
|
||||
model = Motion
|
||||
@ -57,7 +56,7 @@ class MotionSubmitterMixin(forms.ModelForm):
|
||||
"""Mixin to append the submitter field to a MotionForm."""
|
||||
|
||||
submitter = MultiplePersonFormField(label=_("Submitter"))
|
||||
"""Submitter of the Motion. Can be one or more persons."""
|
||||
"""Submitter of the motion. Can be one or more persons."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Fill in the submitter of the motion as default value."""
|
||||
@ -71,7 +70,7 @@ class MotionSupporterMixin(forms.ModelForm):
|
||||
"""Mixin to append the supporter field to a Motionform."""
|
||||
|
||||
supporter = MultiplePersonFormField(required=False, label=_("Supporters"))
|
||||
"""Supporter of the Motion. Can be one or more persons."""
|
||||
"""Supporter of the motion. Can be one or more persons."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Fill in the supporter of the motions as default value."""
|
||||
@ -81,12 +80,12 @@ class MotionSupporterMixin(forms.ModelForm):
|
||||
super(MotionSupporterMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MotionCreateNewVersionMixin(forms.ModelForm):
|
||||
"""Mixin to add the option to the form, to choose, to create a new version."""
|
||||
class MotionDisableVersioningMixin(forms.ModelForm):
|
||||
"""Mixin to add the option to the form to choose to disable versioning."""
|
||||
|
||||
new_version = forms.BooleanField(
|
||||
required=False, label=_("Create new version"), initial=True,
|
||||
help_text=_("Trivial changes don't create a new version."))
|
||||
disable_versioning = forms.BooleanField(
|
||||
required=False, label=_("Don't create a new version"),
|
||||
help_text=_("Don't create a new version. Useful e. g. for trivial changes."))
|
||||
"""BooleanField to decide, if a new version will be created, or the
|
||||
last_version will be used."""
|
||||
|
||||
@ -131,18 +130,13 @@ class ConfigForm(CssClassMixin, forms.Form):
|
||||
label=_("Preamble text for PDF document (all motions)")
|
||||
)
|
||||
|
||||
motion_create_new_version = forms.ChoiceField(
|
||||
widget=forms.Select(),
|
||||
label=_("Create new versions"),
|
||||
motion_allow_disable_versioning = forms.BooleanField(
|
||||
label=_("Allow to disable versioning"),
|
||||
required=False,
|
||||
choices=(
|
||||
('ALLWASY_CREATE_NEW_VERSION', _('create allways a new versions')),
|
||||
('NEVER_CREATE_NEW_VERSION', _('create never a new version')),
|
||||
('ASK_USER', _('Let the user choose if he wants to create a new version')))
|
||||
)
|
||||
|
||||
motion_workflow = forms.ChoiceField(
|
||||
widget=forms.Select(),
|
||||
label=_("Workflow for the motions"),
|
||||
label=_("Workflow of new motions"),
|
||||
required=True,
|
||||
choices=motion_workflow_choices())
|
||||
choices=[(workflow.pk, workflow.name) for workflow in Workflow.objects.all()])
|
||||
|
@ -33,8 +33,7 @@ from openslides.projector.api import register_slidemodel
|
||||
from openslides.projector.models import SlideMixin
|
||||
from openslides.agenda.models import Item
|
||||
|
||||
from .workflow import (motion_workflow_choices, get_state, State, WorkflowError,
|
||||
DUMMY_STATE)
|
||||
from .exceptions import MotionError, WorkflowError
|
||||
|
||||
|
||||
class Motion(SlideMixin, models.Model):
|
||||
@ -43,7 +42,7 @@ class Motion(SlideMixin, models.Model):
|
||||
This class is the main entry point to all other classes related to a motion.
|
||||
"""
|
||||
|
||||
prefix = "motion"
|
||||
prefix = 'motion'
|
||||
"""Prefix for the slide system."""
|
||||
|
||||
active_version = models.ForeignKey('MotionVersion', null=True,
|
||||
@ -55,11 +54,10 @@ class Motion(SlideMixin, models.Model):
|
||||
version. Like the Sighted versions on Wikipedia.
|
||||
"""
|
||||
|
||||
state_id = models.CharField(max_length=3)
|
||||
"""The id of a state object.
|
||||
state = models.ForeignKey('State', null=True) # TODO: Check whether null=True is necessary.
|
||||
"""The related state object.
|
||||
|
||||
This Attribute is used be motion.state to identify the current state of the
|
||||
motion.
|
||||
This attribute is to get the current state of the motion.
|
||||
"""
|
||||
|
||||
identifier = models.CharField(max_length=255, null=True, blank=True,
|
||||
@ -88,9 +86,9 @@ class Motion(SlideMixin, models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save the motion.
|
||||
|
||||
1. Set the state of a new motion to the default motion.
|
||||
1. Set the state of a new motion to the default state.
|
||||
2. Save the motion object.
|
||||
3. Save the version Data.
|
||||
3. Save the version data.
|
||||
4. Set the active version for the motion.
|
||||
|
||||
A new version will be saved if motion.new_version was called
|
||||
@ -105,7 +103,7 @@ class Motion(SlideMixin, models.Model):
|
||||
the config 'motion_create_new_version' is set to
|
||||
'ALLWASY_CREATE_NEW_VERSION'.
|
||||
"""
|
||||
if not self.state_id:
|
||||
if not self.state:
|
||||
self.reset_state()
|
||||
|
||||
super(Motion, self).save(*args, **kwargs)
|
||||
@ -121,11 +119,12 @@ class Motion(SlideMixin, models.Model):
|
||||
else:
|
||||
new_data = False
|
||||
|
||||
need_new_version = config['motion_create_new_version'] == 'ALLWASY_CREATE_NEW_VERSION'
|
||||
# TODO: Check everything here. The decision whether to create a new version has to be done in the view. Update docstings too.
|
||||
need_new_version = self.state.versioning
|
||||
if hasattr(self, '_new_version') or (new_data and need_new_version):
|
||||
version = self.new_version
|
||||
del self._new_version
|
||||
version.motion = self # Test if this line is realy neccessary.
|
||||
version.motion = self # TODO: Test if this line is really neccessary.
|
||||
elif new_data and not need_new_version:
|
||||
version = self.last_version
|
||||
else:
|
||||
@ -150,16 +149,16 @@ class Motion(SlideMixin, models.Model):
|
||||
version.version_number = version_number + 1
|
||||
version.save()
|
||||
|
||||
# Set the active Version of this motion. This has to be done after the
|
||||
# version is saved to the db
|
||||
if not self.state.version_permission or self.active_version is None:
|
||||
# Set the active version of this motion. This has to be done after the
|
||||
# version is saved to the database
|
||||
if not self.state.dont_set_new_version_active or self.active_version is None:
|
||||
self.active_version = version
|
||||
self.save()
|
||||
|
||||
def get_absolute_url(self, link='detail'):
|
||||
"""Return an URL for this version.
|
||||
|
||||
The keywordargument 'link' can be 'detail', 'view', 'edit' or 'delete'.
|
||||
The keyword argument 'link' can be 'detail', 'view', 'edit' or 'delete'.
|
||||
"""
|
||||
if link == 'view' or link == 'detail':
|
||||
return reverse('motion_detail', args=[str(self.id)])
|
||||
@ -240,7 +239,7 @@ class Motion(SlideMixin, models.Model):
|
||||
|
||||
@property
|
||||
def new_version(self):
|
||||
"""Return a Version object, not saved in the database.
|
||||
"""Return a version object, not saved in the database.
|
||||
|
||||
On the first call, it creates a new version. On any later call, it
|
||||
use the existing new version.
|
||||
@ -266,8 +265,8 @@ class Motion(SlideMixin, models.Model):
|
||||
def set_version(self, version):
|
||||
"""Set the 'active' version object.
|
||||
|
||||
The keyargument 'version' can be a MotionVersion object or the
|
||||
version_number of a VersionObject or None.
|
||||
The keyword argument 'version' can be a MotionVersion object or the
|
||||
version_number of a version object or None.
|
||||
|
||||
If the argument is None, the newest version will be used.
|
||||
"""
|
||||
@ -291,7 +290,7 @@ class Motion(SlideMixin, models.Model):
|
||||
@property
|
||||
def last_version(self):
|
||||
"""Return the newest version of the motion."""
|
||||
# TODO: Fix the case, that the motion has no Version
|
||||
# TODO: Fix the case, that the motion has no version
|
||||
try:
|
||||
return self.versions.order_by('-version_number')[0]
|
||||
except IndexError:
|
||||
@ -307,62 +306,39 @@ class Motion(SlideMixin, models.Model):
|
||||
|
||||
def support(self, person):
|
||||
"""Add 'person' as a supporter of this motion."""
|
||||
if self.state.support:
|
||||
if self.state.allow_support:
|
||||
if not self.is_supporter(person):
|
||||
MotionSupporter(motion=self, person=person).save()
|
||||
else:
|
||||
raise WorkflowError("You can not support a motion in state %s" % self.state.name)
|
||||
raise WorkflowError('You can not support a motion in state %s.' % self.state.name)
|
||||
|
||||
def unsupport(self, person):
|
||||
"""Remove 'person' as supporter from this motion."""
|
||||
if self.state.support:
|
||||
if self.state.allow_support:
|
||||
self.supporter.filter(person=person).delete()
|
||||
else:
|
||||
raise WorkflowError("You can not unsupport a motion in state %s" % self.state.name)
|
||||
raise WorkflowError('You can not unsupport a motion in state %s.' % self.state.name)
|
||||
|
||||
def create_poll(self):
|
||||
"""Create a new poll for this motion.
|
||||
|
||||
Return the new poll object.
|
||||
"""
|
||||
if self.state.create_poll:
|
||||
# TODO: auto increment the poll_number in the Database
|
||||
if self.state.allow_create_poll:
|
||||
# TODO: auto increment the poll_number in the database
|
||||
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)
|
||||
|
||||
def get_state(self):
|
||||
"""Return the state of the motion.
|
||||
|
||||
State is a State object. See openslides.motion.workflow for more informations.
|
||||
"""
|
||||
try:
|
||||
return get_state(self.state_id)
|
||||
except WorkflowError:
|
||||
return DUMMY_STATE
|
||||
|
||||
def set_state(self, next_state):
|
||||
"""Set the state of this motion.
|
||||
|
||||
The keyargument 'next_state' has to be a State object or an id of a
|
||||
State object.
|
||||
"""
|
||||
if not isinstance(next_state, State):
|
||||
next_state = get_state(next_state)
|
||||
if next_state in self.state.next_states:
|
||||
self.state_id = next_state.id
|
||||
else:
|
||||
raise WorkflowError('%s is not a valid next_state' % next_state)
|
||||
|
||||
state = property(get_state, set_state)
|
||||
"""The state of the motion as Ste object."""
|
||||
raise WorkflowError('You can not create a poll in state %s.' % self.state.name)
|
||||
|
||||
def reset_state(self):
|
||||
"""Set the state to the default state."""
|
||||
self.state_id = get_state('default').id
|
||||
"""Set the state to the default state. If the motion is new, it chooses the default workflow from config."""
|
||||
if self.state:
|
||||
self.state = self.state.workflow.first_state
|
||||
else:
|
||||
self.state = Workflow.objects.get(pk=config['motion_workflow']).first_state
|
||||
|
||||
def slide(self):
|
||||
"""Return the slide dict."""
|
||||
@ -395,13 +371,13 @@ class Motion(SlideMixin, models.Model):
|
||||
"""
|
||||
actions = {
|
||||
'edit': ((self.is_submitter(person) and
|
||||
self.state.edit_as_submitter) or
|
||||
self.state.allow_submitter_edit) or
|
||||
person.has_perm('motion.can_manage_motion')),
|
||||
|
||||
'create_poll': (person.has_perm('motion.can_manage_motion') and
|
||||
self.state.create_poll),
|
||||
self.state.allow_create_poll),
|
||||
|
||||
'support': (self.state.support and
|
||||
'support': (self.state.allow_support and
|
||||
config['motion_min_supporters'] > 0 and
|
||||
not self.is_submitter(person)),
|
||||
|
||||
@ -410,7 +386,7 @@ class Motion(SlideMixin, models.Model):
|
||||
}
|
||||
actions['delete'] = actions['edit'] # TODO: Only if the motion has no number
|
||||
actions['unsupport'] = actions['support']
|
||||
actions['reset_state'] = 'change_state'
|
||||
actions['reset_state'] = actions['change_state']
|
||||
return actions
|
||||
|
||||
def write_log(self, message, person=None):
|
||||
@ -455,7 +431,7 @@ class MotionVersion(models.Model):
|
||||
A MotionVersion object saves some date of the motion."""
|
||||
|
||||
motion = models.ForeignKey(Motion, related_name='versions')
|
||||
"""The Motion, to witch the version belongs."""
|
||||
"""The motion to which the version belongs."""
|
||||
|
||||
version_number = models.PositiveIntegerField(default=1)
|
||||
"""An id for this version in realation to a motion.
|
||||
@ -464,7 +440,7 @@ class MotionVersion(models.Model):
|
||||
"""
|
||||
|
||||
title = models.CharField(max_length=255, verbose_name=ugettext_lazy("Title"))
|
||||
"""The Title of a motion."""
|
||||
"""The title of a motion."""
|
||||
|
||||
text = models.TextField(verbose_name=_("Text"))
|
||||
"""The text of a motion."""
|
||||
@ -473,10 +449,10 @@ class MotionVersion(models.Model):
|
||||
"""The reason for a motion."""
|
||||
|
||||
rejected = models.BooleanField(default=False)
|
||||
"""Saves, if the version is rejected."""
|
||||
"""Saves if the version is rejected."""
|
||||
|
||||
creation_time = models.DateTimeField(auto_now=True)
|
||||
"""Time, when the version was saved."""
|
||||
"""Time when the version was saved."""
|
||||
|
||||
#identifier = models.CharField(max_length=255, verbose_name=ugettext_lazy("Version identifier"))
|
||||
#note = models.TextField(null=True, blank=True)
|
||||
@ -487,7 +463,7 @@ class MotionVersion(models.Model):
|
||||
def __unicode__(self):
|
||||
"""Return a string, representing this object."""
|
||||
counter = self.version_number or _('new')
|
||||
return "%s Version %s" % (self.motion, counter)
|
||||
return "%s Version %s" % (self.motion, counter) # TODO: Should this really be self.motion or the title of the specific version?
|
||||
|
||||
def get_absolute_url(self, link='detail'):
|
||||
"""Return the URL of this Version.
|
||||
@ -577,11 +553,6 @@ class MotionLog(models.Model):
|
||||
return "%s %s by %s" % (self.time, _(self.message), self.person)
|
||||
|
||||
|
||||
class MotionError(Exception):
|
||||
"""Exception raised when errors in the motion accure."""
|
||||
pass
|
||||
|
||||
|
||||
class MotionVote(BaseVote):
|
||||
"""Saves the votes for a MotionPoll.
|
||||
|
||||
@ -652,3 +623,100 @@ class MotionPoll(CountInvalid, CountVotesCast, BasePoll):
|
||||
"""Apend the fields for invalid and votecast to the ModelForm."""
|
||||
CountInvalid.append_pollform_fields(self, fields)
|
||||
CountVotesCast.append_pollform_fields(self, fields)
|
||||
|
||||
|
||||
class State(models.Model):
|
||||
"""Defines a state for a motion.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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."""
|
||||
|
||||
workflow = models.ForeignKey('Workflow')
|
||||
"""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."""
|
||||
|
||||
icon = models.CharField(max_length=255)
|
||||
"""A string representing the url to the icon-image."""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
dont_set_new_version_active = models.BooleanField(default=False)
|
||||
"""If true, new versions are not automaticly set active."""
|
||||
|
||||
def __unicode__(self):
|
||||
"""Returns the name of the state."""
|
||||
return self.name
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""Saves a state to the database.
|
||||
|
||||
Used to check the integrity before saving.
|
||||
"""
|
||||
self.check_next_states()
|
||||
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))
|
||||
|
||||
|
||||
class Workflow(models.Model):
|
||||
"""Defines a workflow for a motion."""
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
"""A string representing the workflow."""
|
||||
|
||||
first_state = models.OneToOneField(State, related_name='+')
|
||||
"""A one-to-one relation to a state, the starting point for the workflow."""
|
||||
|
||||
def __unicode__(self):
|
||||
"""Returns the name of the workflow."""
|
||||
return self.name
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""Saves a workflow to the database.
|
||||
|
||||
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."""
|
||||
if not self.first_state.workflow == self:
|
||||
raise WorkflowError('%s can not be first state of %s because it does not belong to it.' % (self.first_state, self))
|
||||
|
@ -5,6 +5,9 @@
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Functions to generate the PDFs for the motion app.
|
||||
|
||||
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||
:license: GNU GPL, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from reportlab.lib import colors
|
||||
|
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
openslides.motion.signales
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
openslides.motion.signals
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Signals for the motion app.
|
||||
|
||||
@ -26,5 +26,5 @@ def default_config(sender, key, **kwargs):
|
||||
'motion_pdf_ballot_papers_number': '8',
|
||||
'motion_pdf_title': _('Motions'),
|
||||
'motion_pdf_preamble': '',
|
||||
'motion_create_new_version': 'ALLWASY_CREATE_NEW_VERSION',
|
||||
'motion_workflow': 'default'}.get(key)
|
||||
'motion_allow_disable_versioning': False,
|
||||
'motion_workflow': 1}.get(key)
|
||||
|
@ -4,7 +4,7 @@
|
||||
openslides.motion.slides
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Defines the Slides for the motion app.
|
||||
Defines the slides for the motion app.
|
||||
|
||||
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||
:license: GNU GPL, see LICENSE for more details.
|
||||
|
@ -16,8 +16,8 @@
|
||||
<p>State: {{ motion.state }}</p>
|
||||
<h4>possible stats:</h4>
|
||||
<ul>
|
||||
{% for state in motion.state.next_states %}
|
||||
<li><a href="{% url 'motion_set_state' motion.pk state.id %}">{{ state }}</a></li>
|
||||
{% for state in motion.state.next_states.all %}
|
||||
<li><a href="{% url 'motion_set_state' motion.pk state.pk %}">{{ state }}</a></li>
|
||||
{% endfor %}
|
||||
<li><a href="{% url 'motion_reset_state' motion.pk %}">Reset State</a></li>
|
||||
</ul>
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
from django.conf.urls import url, patterns
|
||||
|
||||
|
||||
urlpatterns = patterns('openslides.motion.views',
|
||||
url(r'^$',
|
||||
'motion_list',
|
||||
@ -78,7 +79,7 @@ urlpatterns = patterns('openslides.motion.views',
|
||||
name='motion_poll_delete',
|
||||
),
|
||||
|
||||
url(r'^(?P<pk>\d+)/set_state/(?P<state>[a-z]{3})/$',
|
||||
url(r'^(?P<pk>\d+)/set_state/(?P<state>\d+)/$',
|
||||
'set_state',
|
||||
name='motion_set_state',
|
||||
),
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
Views for the motion app.
|
||||
|
||||
Will automaticly imported from openslides.motion.urls.py
|
||||
The views are automaticly imported from openslides.motion.urls.
|
||||
|
||||
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||
:license: GNU GPL, see LICENSE for more details.
|
||||
@ -32,10 +32,9 @@ from openslides.projector.projector import Widget, SLIDE
|
||||
from openslides.config.models import config
|
||||
from openslides.agenda.models import Item
|
||||
|
||||
from .models import Motion, MotionSubmitter, MotionSupporter, MotionPoll, MotionVersion
|
||||
from .models import Motion, MotionSubmitter, MotionSupporter, MotionPoll, MotionVersion, State, WorkflowError
|
||||
from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin,
|
||||
MotionCreateNewVersionMixin, ConfigForm)
|
||||
from .workflow import WorkflowError
|
||||
MotionDisableVersioningMixin, ConfigForm)
|
||||
from .pdf import motions_to_pdf, motion_to_pdf
|
||||
|
||||
|
||||
@ -81,7 +80,7 @@ motion_detail = MotionDetailView.as_view()
|
||||
|
||||
|
||||
class MotionMixin(object):
|
||||
"""Mixin for MotionViewsClasses, to save the version data."""
|
||||
"""Mixin for MotionViewsClasses to save the version data."""
|
||||
|
||||
def manipulate_object(self, form):
|
||||
"""Save the version data into the motion object before it is saved in
|
||||
@ -125,8 +124,9 @@ class MotionMixin(object):
|
||||
form_classes.append(MotionSubmitterMixin)
|
||||
if config['motion_min_supporters'] > 0:
|
||||
form_classes.append(MotionSupporterMixin)
|
||||
if config['motion_create_new_version'] == 'ASK_USER':
|
||||
form_classes.append(MotionCreateNewVersionMixin)
|
||||
if self.object:
|
||||
if config['motion_allow_disable_versioning'] and self.object.state.versioning:
|
||||
form_classes.append(MotionDisableVersioningMixin)
|
||||
return type('MotionForm', tuple(form_classes), {})
|
||||
|
||||
|
||||
@ -267,7 +267,7 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
|
||||
def case_yes(self):
|
||||
"""Append or remove the request.user from the motion.
|
||||
|
||||
First the methode checks the permissions, and writes a log message after
|
||||
First the method checks the permissions, and writes a log message after
|
||||
appending or removing the user.
|
||||
"""
|
||||
if self.check_permission(self.request):
|
||||
@ -395,8 +395,8 @@ class MotionSetStateView(SingleObjectMixin, RedirectView):
|
||||
if self.reset:
|
||||
self.object.reset_state()
|
||||
else:
|
||||
self.object.state = kwargs['state']
|
||||
except WorkflowError, e:
|
||||
self.object.state = State.objects.get(pk=kwargs['state'])
|
||||
except WorkflowError, e: # TODO: Is a WorkflowError still possible here?
|
||||
messages.error(request, e)
|
||||
else:
|
||||
self.object.save()
|
||||
@ -484,7 +484,7 @@ class Config(FormView):
|
||||
'motion_pdf_ballot_papers_number': config['motion_pdf_ballot_papers_number'],
|
||||
'motion_pdf_title': config['motion_pdf_title'],
|
||||
'motion_pdf_preamble': config['motion_pdf_preamble'],
|
||||
'motion_create_new_version': config['motion_create_new_version'],
|
||||
'motion_allow_disable_versioning': config['motion_allow_disable_versioning'],
|
||||
'motion_workflow': config['motion_workflow'],
|
||||
}
|
||||
|
||||
@ -495,7 +495,7 @@ class Config(FormView):
|
||||
config['motion_pdf_ballot_papers_number'] = form.cleaned_data['motion_pdf_ballot_papers_number']
|
||||
config['motion_pdf_title'] = form.cleaned_data['motion_pdf_title']
|
||||
config['motion_pdf_preamble'] = form.cleaned_data['motion_pdf_preamble']
|
||||
config['motion_create_new_version'] = form.cleaned_data['motion_create_new_version']
|
||||
config['motion_allow_disable_versioning'] = form.cleaned_data['motion_allow_disable_versioning']
|
||||
config['motion_workflow'] = form.cleaned_data['motion_workflow']
|
||||
messages.success(self.request, _('Motion settings successfully saved.'))
|
||||
return super(Config, self).form_valid(form)
|
||||
|
@ -1,144 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
openslides.utils.workflow
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
openslides.motion.workflow
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Defines the States for motions. All States are linked together with there
|
||||
'next_state' attributes. Together there are a workflow.
|
||||
This file is only for development. It will be moved out of
|
||||
the openslides module before the next release.
|
||||
|
||||
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||
:license: GNU GPL, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import exceptions
|
||||
from django.utils.importlib import import_module
|
||||
from django.utils.translation import ugettext_noop
|
||||
|
||||
from openslides.config.models import config
|
||||
|
||||
_workflow = None
|
||||
from .models import Workflow, State
|
||||
|
||||
|
||||
class State(object):
|
||||
"""Define a state for a motion."""
|
||||
def __init__(self, id, name, next_states=[], create_poll=False, support=False,
|
||||
edit_as_submitter=False, version_permission=True):
|
||||
"""Set attributes for the state.
|
||||
|
||||
The Arguments are:
|
||||
- 'id' a unique id for the state.
|
||||
- 'name' a string representing the state.
|
||||
- 'next_states' a list with all states, that can be choosen from this state.
|
||||
|
||||
All the other arguments are boolean values. If True, the specific action for
|
||||
motions in this state.
|
||||
- 'create_poll': polls can be created in this state.
|
||||
- 'support': persons can support the motion in this state.
|
||||
- 'edit_as_submitter': the submitter can edit the motion in this state.
|
||||
- 'version_permission': new versions are not permitted.
|
||||
def _init_builtin_workflows():
|
||||
"""
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.next_states = next_states
|
||||
self.create_poll = create_poll
|
||||
self.support = support
|
||||
self.edit_as_submitter = edit_as_submitter
|
||||
self.version_permission = version_permission
|
||||
|
||||
def __unicode__(self):
|
||||
"""Return the name of the state."""
|
||||
return self.name
|
||||
|
||||
|
||||
class WorkflowError(Exception):
|
||||
"""Exception raised when errors in a state accure."""
|
||||
pass
|
||||
|
||||
|
||||
def motion_workflow_choices():
|
||||
"""Return all possible workflows.
|
||||
|
||||
The possible workflows can be set in the settings with the setting
|
||||
'MOTION_WORKFLOW'.
|
||||
Saves a simple and a complex workflow into the database.
|
||||
This function is only called manually and lives here only for development.
|
||||
"""
|
||||
for workflow in settings.MOTION_WORKFLOW:
|
||||
yield workflow[0], workflow[1]
|
||||
workflow_1 = Workflow(name=ugettext_noop('Simple Workflow'), id=1)
|
||||
state_1_1 = State.objects.create(name=ugettext_noop('submitted'), workflow=workflow_1,
|
||||
allow_create_poll=True, allow_support=True, allow_submitter_edit=True)
|
||||
state_1_2 = State.objects.create(name=ugettext_noop('accepted'), workflow=workflow_1, action_word=ugettext_noop('accept'))
|
||||
state_1_3 = State.objects.create(name=ugettext_noop('rejected'), workflow=workflow_1, action_word=ugettext_noop('reject'))
|
||||
state_1_4 = State.objects.create(name=ugettext_noop('not decided'), workflow=workflow_1, action_word=ugettext_noop('do not decide'))
|
||||
state_1_1.next_states.add(state_1_2, state_1_3, state_1_4)
|
||||
state_1_1.save() # Is this neccessary?
|
||||
workflow_1.first_state = state_1_1
|
||||
workflow_1.save()
|
||||
|
||||
|
||||
def get_state(state='default'):
|
||||
"""Return a state object.
|
||||
|
||||
The argument 'state' has to be a state_id.
|
||||
|
||||
If the argument 'state' is 'default', the default state is returned.
|
||||
|
||||
The default state is the state object choosen in the config tab.
|
||||
"""
|
||||
global _workflow
|
||||
if _workflow is not None:
|
||||
try:
|
||||
return _workflow[state]
|
||||
except KeyError:
|
||||
raise WorkflowError('Unknown state: %s' % state)
|
||||
_workflow = {}
|
||||
for workflow in settings.MOTION_WORKFLOW:
|
||||
if workflow[0] == config['motion_workflow']:
|
||||
try:
|
||||
wf_module, wf_default_state_name = workflow[2].rsplit('.', 1)
|
||||
except ValueError:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
'%s isn\'t a workflow module' % workflow[2])
|
||||
try:
|
||||
mod = import_module(wf_module)
|
||||
except ImportError as e:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
'Error importing workflow %s: "%s"' % (wf_module, e))
|
||||
try:
|
||||
default_state = getattr(mod, wf_default_state_name)
|
||||
except AttributeError:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
'Workflow module "%s" does not define a "%s" State'
|
||||
% (wf_module, wf_default_state_name))
|
||||
_workflow['default'] = default_state
|
||||
break
|
||||
else:
|
||||
raise ImproperlyConfigured('Unknown workflow %s' % conf['motion_workflow'])
|
||||
|
||||
populate_workflow(default_state, _workflow)
|
||||
return get_state(state)
|
||||
|
||||
|
||||
def populate_workflow(state, workflow):
|
||||
"""Append all 'next_states' from state to the workflow.
|
||||
|
||||
The argument state has to be a state object.
|
||||
|
||||
The argument workflow has to be a dictonary.
|
||||
|
||||
Calls this function recrusiv with all next_states from the next_states states.
|
||||
"""
|
||||
workflow[state.id] = state
|
||||
for s in state.next_states:
|
||||
if s.id not in workflow:
|
||||
populate_workflow(s, workflow)
|
||||
|
||||
|
||||
DUMMY_STATE = State('dummy', ugettext_noop('Unknwon state'))
|
||||
"""A dummy state object. Returned, if the state_id is not known."""
|
||||
|
||||
default_workflow = State('pub', ugettext_noop('Published'), support=True,
|
||||
edit_as_submitter=True, version_permission=False)
|
||||
"""Default Workflow for OpenSlides."""
|
||||
|
||||
default_workflow.next_states = [
|
||||
State('per', ugettext_noop('Permitted'), create_poll=True, edit_as_submitter=True, next_states=[
|
||||
State('acc', ugettext_noop('Accepted')),
|
||||
State('rej', ugettext_noop('Rejected')),
|
||||
State('wit', ugettext_noop('Withdrawed')),
|
||||
State('adj', ugettext_noop('Adjourned')),
|
||||
State('noc', ugettext_noop('Not Concerned')),
|
||||
State('com', ugettext_noop('Commited a bill')),
|
||||
State('rev', ugettext_noop('Needs Review'))]),
|
||||
State('nop', ugettext_noop('Rejected (not authorized)'))]
|
||||
workflow_2 = Workflow(name=ugettext_noop('Complex Workflow'), id=2)
|
||||
state_2_1 = State.objects.create(name=ugettext_noop('published'), workflow=workflow_2, allow_support=True, allow_submitter_edit=True)
|
||||
state_2_2 = State.objects.create(name=ugettext_noop('permitted'), workflow=workflow_2, action_word=ugettext_noop('permit'),
|
||||
allow_create_poll=True, allow_submitter_edit=True, versioning=True, dont_set_new_version_active=True)
|
||||
state_2_3 = State.objects.create(name=ugettext_noop('accepted'), workflow=workflow_2, action_word=ugettext_noop('accept'), versioning=True)
|
||||
state_2_4 = State.objects.create(name=ugettext_noop('rejected'), workflow=workflow_2, action_word=ugettext_noop('reject'), versioning=True)
|
||||
state_2_5 = State.objects.create(name=ugettext_noop('withdrawed'), workflow=workflow_2,
|
||||
action_word=ugettext_noop('withdraw'), versioning=True)
|
||||
state_2_6 = State.objects.create(name=ugettext_noop('adjourned'), workflow=workflow_2, action_word=ugettext_noop('adjourn'), versioning=True)
|
||||
state_2_7 = State.objects.create(name=ugettext_noop('not concerned'), workflow=workflow_2, versioning=True)
|
||||
state_2_8 = State.objects.create(name=ugettext_noop('commited a bill'), workflow=workflow_2,
|
||||
action_word=ugettext_noop('commit a bill'), versioning=True)
|
||||
state_2_9 = State.objects.create(name=ugettext_noop('needs review'), workflow=workflow_2, versioning=True)
|
||||
state_2_10 = State.objects.create(name=ugettext_noop('rejected (not authorized)'), workflow=workflow_2,
|
||||
action_word=ugettext_noop('reject (not authorized)'), versioning=True)
|
||||
state_2_1.next_states.add(state_2_2, state_2_5, state_2_10)
|
||||
state_2_2.next_states.add(state_2_3, state_2_4, state_2_5, state_2_6, state_2_7, state_2_8, state_2_9)
|
||||
state_2_1.save() # Is this neccessary?
|
||||
state_2_2.save() # Is this neccessary?
|
||||
workflow_2.first_state = state_2_1
|
||||
workflow_2.save()
|
||||
|
15
openslides/utils/exceptions.py
Normal file
15
openslides/utils/exceptions.py
Normal file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
openslides.utils.exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Base Exception for OpenSlides.
|
||||
|
||||
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS.
|
||||
:license: GNU GPL, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
|
||||
class OpenSlidesError(Exception):
|
||||
pass
|
@ -12,17 +12,17 @@ from django.test import TestCase
|
||||
|
||||
from openslides.participant.models import User
|
||||
from openslides.config.models import config
|
||||
from openslides.motion.models import Motion
|
||||
from openslides.motion.workflow import WorkflowError
|
||||
from openslides.motion.models import Motion, Workflow, State
|
||||
from openslides.motion.exceptions import WorkflowError
|
||||
|
||||
|
||||
class ModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.motion = Motion.objects.create(title='v1')
|
||||
self.test_user = User.objects.create(username='blub')
|
||||
self.workflow = Workflow.objects.get(pk=1)
|
||||
|
||||
def test_create_new_version(self):
|
||||
config['motion_create_new_version'] = 'ALLWASY_CREATE_NEW_VERSION'
|
||||
motion = Motion.objects.create(title='m1')
|
||||
self.assertEqual(motion.versions.count(), 1)
|
||||
|
||||
@ -32,13 +32,16 @@ class ModelTest(TestCase):
|
||||
|
||||
motion.title = 'new title'
|
||||
motion.save()
|
||||
self.assertEqual(motion.versions.count(), 3)
|
||||
self.assertEqual(motion.versions.count(), 2)
|
||||
|
||||
motion.save()
|
||||
self.assertEqual(motion.versions.count(), 2)
|
||||
|
||||
motion.state = State.objects.create(name='automatic_versioning', workflow=self.workflow, versioning=True)
|
||||
motion.text = 'new text'
|
||||
motion.save()
|
||||
self.assertEqual(motion.versions.count(), 3)
|
||||
|
||||
config['motion_create_new_version'] = 'NEVER_CREATE_NEW_VERSION'
|
||||
motion.text = 'new text'
|
||||
motion.save()
|
||||
self.assertEqual(motion.versions.count(), 3)
|
||||
|
||||
@ -56,6 +59,7 @@ class ModelTest(TestCase):
|
||||
|
||||
def test_version(self):
|
||||
motion = Motion.objects.create(title='v1')
|
||||
motion.state = State.objects.create(name='automatic_versioning', workflow=self.workflow, versioning=True)
|
||||
motion.title = 'v2'
|
||||
motion.save()
|
||||
v2_version = motion.version
|
||||
@ -94,24 +98,42 @@ class ModelTest(TestCase):
|
||||
self.motion.unsupport(self.test_user)
|
||||
|
||||
def test_poll(self):
|
||||
self.motion.state = 'per'
|
||||
self.motion.state = State.objects.get(pk=1)
|
||||
poll = self.motion.create_poll()
|
||||
self.assertEqual(poll.poll_number, 1)
|
||||
|
||||
def test_state(self):
|
||||
self.motion.reset_state()
|
||||
self.assertEqual(self.motion.state.id, 'pub')
|
||||
self.assertEqual(self.motion.state.name, 'submitted')
|
||||
|
||||
self.motion.state = State.objects.get(pk=5)
|
||||
self.assertEqual(self.motion.state.name, 'published')
|
||||
with self.assertRaises(WorkflowError):
|
||||
self.motion.create_poll()
|
||||
|
||||
self.motion.set_state('per')
|
||||
self.assertEqual(self.motion.state.id, 'per')
|
||||
self.motion.state = State.objects.get(pk=6)
|
||||
self.assertEqual(self.motion.state.name, 'permitted')
|
||||
self.assertEqual(self.motion.state.get_action_word(), 'permit')
|
||||
with self.assertRaises(WorkflowError):
|
||||
self.motion.support(self.test_user)
|
||||
with self.assertRaises(WorkflowError):
|
||||
self.motion.unsupport(self.test_user)
|
||||
|
||||
with self.assertRaises(WorkflowError):
|
||||
self.motion.set_state('per')
|
||||
def test_new_states_or_workflows(self):
|
||||
workflow_1 = Workflow(name='W1', id=1000)
|
||||
state_1 = State.objects.create(name='S1', workflow=workflow_1)
|
||||
workflow_1.first_state = state_1
|
||||
workflow_1.save()
|
||||
workflow_2 = Workflow(name='W2', id=2000)
|
||||
state_2 = State.objects.create(name='S2', workflow=workflow_2)
|
||||
workflow_2.first_state = state_2
|
||||
workflow_2.save()
|
||||
state_3 = State.objects.create(name='S3', workflow=workflow_1)
|
||||
|
||||
with self.assertRaises(WorkflowError):
|
||||
workflow_2.first_state = state_3
|
||||
workflow_2.save()
|
||||
|
||||
with self.assertRaises(WorkflowError):
|
||||
state_1.next_states.add(state_2)
|
||||
state_1.save()
|
||||
|
Loading…
Reference in New Issue
Block a user