Work on motion workflow system.
Also: Insert new base exception for OpenSlides. Also: Insert a workflow field to the state class. Update tests. Rename versioning attribute.
This commit is contained in:
parent
44ea7c835d
commit
f2dde228c9
@ -28,10 +28,6 @@ SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
|||||||
|
|
||||||
ugettext = lambda s: s
|
ugettext = lambda s: s
|
||||||
|
|
||||||
MOTION_WORKFLOW = (
|
|
||||||
('default', ugettext('default'), 'openslides.motion.workflow.default_workflow'),
|
|
||||||
)
|
|
||||||
|
|
||||||
LANGUAGES = (
|
LANGUAGES = (
|
||||||
('de', ugettext('German')),
|
('de', ugettext('German')),
|
||||||
('en', ugettext('English')),
|
('en', ugettext('English')),
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
openslides.motion
|
openslides.motion
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The OpenSlides motion app appends the functionality to OpenSlides, to
|
The OpenSlides motion app appends the functionality to OpenSlides to
|
||||||
manage motions.
|
manage motions.
|
||||||
|
|
||||||
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||||
|
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.forms import CssClassMixin
|
||||||
from openslides.utils.person import PersonFormField, MultiplePersonFormField
|
from openslides.utils.person import PersonFormField, MultiplePersonFormField
|
||||||
from .models import Motion
|
from .models import Motion, Workflow
|
||||||
from .workflow import motion_workflow_choices
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMotionForm(forms.ModelForm, CssClassMixin):
|
class BaseMotionForm(forms.ModelForm, CssClassMixin):
|
||||||
"""Base FormClass for a Motion.
|
"""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.
|
submitter, supporters etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title = forms.CharField(widget=forms.TextInput(), label=_("Title"))
|
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 = 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(
|
reason = forms.CharField(
|
||||||
widget=forms.Textarea(), required=False, label=_("Reason"))
|
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:
|
class Meta:
|
||||||
model = Motion
|
model = Motion
|
||||||
@ -57,7 +56,7 @@ class MotionSubmitterMixin(forms.ModelForm):
|
|||||||
"""Mixin to append the submitter field to a MotionForm."""
|
"""Mixin to append the submitter field to a MotionForm."""
|
||||||
|
|
||||||
submitter = MultiplePersonFormField(label=_("Submitter"))
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Fill in the submitter of the motion as default value."""
|
"""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."""
|
"""Mixin to append the supporter field to a Motionform."""
|
||||||
|
|
||||||
supporter = MultiplePersonFormField(required=False, label=_("Supporters"))
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Fill in the supporter of the motions as default value."""
|
"""Fill in the supporter of the motions as default value."""
|
||||||
@ -81,12 +80,12 @@ class MotionSupporterMixin(forms.ModelForm):
|
|||||||
super(MotionSupporterMixin, self).__init__(*args, **kwargs)
|
super(MotionSupporterMixin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class MotionCreateNewVersionMixin(forms.ModelForm):
|
class MotionDisableVersioningMixin(forms.ModelForm):
|
||||||
"""Mixin to add the option to the form, to choose, to create a new version."""
|
"""Mixin to add the option to the form to choose to disable versioning."""
|
||||||
|
|
||||||
new_version = forms.BooleanField(
|
disable_versioning = forms.BooleanField(
|
||||||
required=False, label=_("Create new version"), initial=True,
|
required=False, label=_("Don't create a new version"),
|
||||||
help_text=_("Trivial changes 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
|
"""BooleanField to decide, if a new version will be created, or the
|
||||||
last_version will be used."""
|
last_version will be used."""
|
||||||
|
|
||||||
@ -131,18 +130,13 @@ class ConfigForm(CssClassMixin, forms.Form):
|
|||||||
label=_("Preamble text for PDF document (all motions)")
|
label=_("Preamble text for PDF document (all motions)")
|
||||||
)
|
)
|
||||||
|
|
||||||
motion_create_new_version = forms.ChoiceField(
|
motion_allow_disable_versioning = forms.BooleanField(
|
||||||
widget=forms.Select(),
|
label=_("Allow to disable versioning"),
|
||||||
label=_("Create new versions"),
|
|
||||||
required=False,
|
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(
|
motion_workflow = forms.ChoiceField(
|
||||||
widget=forms.Select(),
|
widget=forms.Select(),
|
||||||
label=_("Workflow for the motions"),
|
label=_("Workflow of new motions"),
|
||||||
required=True,
|
required=True,
|
||||||
choices=motion_workflow_choices())
|
choices=[(workflow.pk, workflow.name) for workflow in Workflow.objects.all()])
|
||||||
|
@ -24,6 +24,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop, ugettext as _
|
|||||||
|
|
||||||
from openslides.utils.utils import _propper_unicode
|
from openslides.utils.utils import _propper_unicode
|
||||||
from openslides.utils.person import PersonField
|
from openslides.utils.person import PersonField
|
||||||
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.config.models import config
|
from openslides.config.models import config
|
||||||
from openslides.config.signals import default_config_value
|
from openslides.config.signals import default_config_value
|
||||||
from openslides.poll.models import (
|
from openslides.poll.models import (
|
||||||
@ -33,8 +34,10 @@ from openslides.projector.api import register_slidemodel
|
|||||||
from openslides.projector.models import SlideMixin
|
from openslides.projector.models import SlideMixin
|
||||||
from openslides.agenda.models import Item
|
from openslides.agenda.models import Item
|
||||||
|
|
||||||
from .workflow import (motion_workflow_choices, get_state, State, WorkflowError,
|
|
||||||
DUMMY_STATE)
|
class MotionError(OpenSlidesError):
|
||||||
|
"""Exception raised when errors in the motion accure."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Motion(SlideMixin, models.Model):
|
class Motion(SlideMixin, models.Model):
|
||||||
@ -43,7 +46,7 @@ class Motion(SlideMixin, models.Model):
|
|||||||
This class is the main entry point to all other classes related to a motion.
|
This class is the main entry point to all other classes related to a motion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
prefix = "motion"
|
prefix = 'motion'
|
||||||
"""Prefix for the slide system."""
|
"""Prefix for the slide system."""
|
||||||
|
|
||||||
active_version = models.ForeignKey('MotionVersion', null=True,
|
active_version = models.ForeignKey('MotionVersion', null=True,
|
||||||
@ -55,11 +58,10 @@ class Motion(SlideMixin, models.Model):
|
|||||||
version. Like the Sighted versions on Wikipedia.
|
version. Like the Sighted versions on Wikipedia.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
state_id = models.CharField(max_length=3)
|
state = models.ForeignKey('State', null=True) # TODO: Check whether null=True is necessary.
|
||||||
"""The id of a state object.
|
"""The related state object.
|
||||||
|
|
||||||
This Attribute is used be motion.state to identify the current state of the
|
This attribute is to get the current state of the motion.
|
||||||
motion.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
identifier = models.CharField(max_length=255, null=True, blank=True,
|
identifier = models.CharField(max_length=255, null=True, blank=True,
|
||||||
@ -88,9 +90,9 @@ class Motion(SlideMixin, models.Model):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Save the motion.
|
"""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.
|
2. Save the motion object.
|
||||||
3. Save the version Data.
|
3. Save the version data.
|
||||||
4. Set the active version for the motion.
|
4. Set the active version for the motion.
|
||||||
|
|
||||||
A new version will be saved if motion.new_version was called
|
A new version will be saved if motion.new_version was called
|
||||||
@ -105,7 +107,7 @@ class Motion(SlideMixin, models.Model):
|
|||||||
the config 'motion_create_new_version' is set to
|
the config 'motion_create_new_version' is set to
|
||||||
'ALLWASY_CREATE_NEW_VERSION'.
|
'ALLWASY_CREATE_NEW_VERSION'.
|
||||||
"""
|
"""
|
||||||
if not self.state_id:
|
if not self.state:
|
||||||
self.reset_state()
|
self.reset_state()
|
||||||
|
|
||||||
super(Motion, self).save(*args, **kwargs)
|
super(Motion, self).save(*args, **kwargs)
|
||||||
@ -121,11 +123,12 @@ class Motion(SlideMixin, models.Model):
|
|||||||
else:
|
else:
|
||||||
new_data = False
|
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):
|
if hasattr(self, '_new_version') or (new_data and need_new_version):
|
||||||
version = self.new_version
|
version = self.new_version
|
||||||
del 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:
|
elif new_data and not need_new_version:
|
||||||
version = self.last_version
|
version = self.last_version
|
||||||
else:
|
else:
|
||||||
@ -150,16 +153,16 @@ class Motion(SlideMixin, models.Model):
|
|||||||
version.version_number = version_number + 1
|
version.version_number = version_number + 1
|
||||||
version.save()
|
version.save()
|
||||||
|
|
||||||
# Set the active Version of this motion. This has to be done after the
|
# Set the active version of this motion. This has to be done after the
|
||||||
# version is saved to the db
|
# version is saved to the database
|
||||||
if not self.state.version_permission or self.active_version is None:
|
if not self.state.dont_set_new_version_active or self.active_version is None:
|
||||||
self.active_version = version
|
self.active_version = version
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def get_absolute_url(self, link='detail'):
|
def get_absolute_url(self, link='detail'):
|
||||||
"""Return an URL for this version.
|
"""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':
|
if link == 'view' or link == 'detail':
|
||||||
return reverse('motion_detail', args=[str(self.id)])
|
return reverse('motion_detail', args=[str(self.id)])
|
||||||
@ -240,7 +243,7 @@ class Motion(SlideMixin, models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def new_version(self):
|
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
|
On the first call, it creates a new version. On any later call, it
|
||||||
use the existing new version.
|
use the existing new version.
|
||||||
@ -266,8 +269,8 @@ class Motion(SlideMixin, models.Model):
|
|||||||
def set_version(self, version):
|
def set_version(self, version):
|
||||||
"""Set the 'active' version object.
|
"""Set the 'active' version object.
|
||||||
|
|
||||||
The keyargument 'version' can be a MotionVersion object or the
|
The keyword argument 'version' can be a MotionVersion object or the
|
||||||
version_number of a VersionObject or None.
|
version_number of a version object or None.
|
||||||
|
|
||||||
If the argument is None, the newest version will be used.
|
If the argument is None, the newest version will be used.
|
||||||
"""
|
"""
|
||||||
@ -291,7 +294,7 @@ class Motion(SlideMixin, models.Model):
|
|||||||
@property
|
@property
|
||||||
def last_version(self):
|
def last_version(self):
|
||||||
"""Return the newest version of the motion."""
|
"""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:
|
try:
|
||||||
return self.versions.order_by('-version_number')[0]
|
return self.versions.order_by('-version_number')[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@ -307,62 +310,39 @@ class Motion(SlideMixin, models.Model):
|
|||||||
|
|
||||||
def support(self, person):
|
def support(self, person):
|
||||||
"""Add 'person' as a supporter of this motion."""
|
"""Add 'person' as a supporter of this motion."""
|
||||||
if self.state.support:
|
if self.state.allow_support:
|
||||||
if not self.is_supporter(person):
|
if not self.is_supporter(person):
|
||||||
MotionSupporter(motion=self, person=person).save()
|
MotionSupporter(motion=self, person=person).save()
|
||||||
else:
|
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):
|
def unsupport(self, person):
|
||||||
"""Remove 'person' as supporter from this motion."""
|
"""Remove 'person' as supporter from this motion."""
|
||||||
if self.state.support:
|
if self.state.allow_support:
|
||||||
self.supporter.filter(person=person).delete()
|
self.supporter.filter(person=person).delete()
|
||||||
else:
|
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):
|
def create_poll(self):
|
||||||
"""Create a new poll for this motion.
|
"""Create a new poll for this motion.
|
||||||
|
|
||||||
Return the new poll object.
|
Return the new poll object.
|
||||||
"""
|
"""
|
||||||
if self.state.create_poll:
|
if self.state.allow_create_poll:
|
||||||
# TODO: auto increment the poll_number in the Database
|
# TODO: auto increment the poll_number in the database
|
||||||
poll_number = self.polls.aggregate(Max('poll_number'))['poll_number__max'] or 0
|
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 = MotionPoll.objects.create(motion=self, poll_number=poll_number + 1)
|
||||||
poll.set_options()
|
poll.set_options()
|
||||||
return poll
|
return poll
|
||||||
else:
|
else:
|
||||||
raise WorkflowError("You can not create a poll in state %s" % self.state.name)
|
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."""
|
|
||||||
|
|
||||||
def reset_state(self):
|
def reset_state(self):
|
||||||
"""Set the state to the default state."""
|
"""Set the state to the default state. If the motion is new, it chooses the default workflow from config."""
|
||||||
self.state_id = get_state('default').id
|
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):
|
def slide(self):
|
||||||
"""Return the slide dict."""
|
"""Return the slide dict."""
|
||||||
@ -395,13 +375,13 @@ class Motion(SlideMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
actions = {
|
actions = {
|
||||||
'edit': ((self.is_submitter(person) and
|
'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')),
|
person.has_perm('motion.can_manage_motion')),
|
||||||
|
|
||||||
'create_poll': (person.has_perm('motion.can_manage_motion') and
|
'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
|
config['motion_min_supporters'] > 0 and
|
||||||
not self.is_submitter(person)),
|
not self.is_submitter(person)),
|
||||||
|
|
||||||
@ -410,7 +390,7 @@ class Motion(SlideMixin, models.Model):
|
|||||||
}
|
}
|
||||||
actions['delete'] = actions['edit'] # TODO: Only if the motion has no number
|
actions['delete'] = actions['edit'] # TODO: Only if the motion has no number
|
||||||
actions['unsupport'] = actions['support']
|
actions['unsupport'] = actions['support']
|
||||||
actions['reset_state'] = 'change_state'
|
actions['reset_state'] = actions['change_state']
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
def write_log(self, message, person=None):
|
def write_log(self, message, person=None):
|
||||||
@ -455,7 +435,7 @@ class MotionVersion(models.Model):
|
|||||||
A MotionVersion object saves some date of the motion."""
|
A MotionVersion object saves some date of the motion."""
|
||||||
|
|
||||||
motion = models.ForeignKey(Motion, related_name='versions')
|
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)
|
version_number = models.PositiveIntegerField(default=1)
|
||||||
"""An id for this version in realation to a motion.
|
"""An id for this version in realation to a motion.
|
||||||
@ -464,7 +444,7 @@ class MotionVersion(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
title = models.CharField(max_length=255, verbose_name=ugettext_lazy("Title"))
|
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"))
|
text = models.TextField(verbose_name=_("Text"))
|
||||||
"""The text of a motion."""
|
"""The text of a motion."""
|
||||||
@ -473,10 +453,10 @@ class MotionVersion(models.Model):
|
|||||||
"""The reason for a motion."""
|
"""The reason for a motion."""
|
||||||
|
|
||||||
rejected = models.BooleanField(default=False)
|
rejected = models.BooleanField(default=False)
|
||||||
"""Saves, if the version is rejected."""
|
"""Saves if the version is rejected."""
|
||||||
|
|
||||||
creation_time = models.DateTimeField(auto_now=True)
|
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"))
|
#identifier = models.CharField(max_length=255, verbose_name=ugettext_lazy("Version identifier"))
|
||||||
#note = models.TextField(null=True, blank=True)
|
#note = models.TextField(null=True, blank=True)
|
||||||
@ -487,7 +467,7 @@ class MotionVersion(models.Model):
|
|||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
"""Return a string, representing this object."""
|
"""Return a string, representing this object."""
|
||||||
counter = self.version_number or _('new')
|
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'):
|
def get_absolute_url(self, link='detail'):
|
||||||
"""Return the URL of this Version.
|
"""Return the URL of this Version.
|
||||||
@ -577,11 +557,6 @@ class MotionLog(models.Model):
|
|||||||
return "%s %s by %s" % (self.time, _(self.message), self.person)
|
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):
|
class MotionVote(BaseVote):
|
||||||
"""Saves the votes for a MotionPoll.
|
"""Saves the votes for a MotionPoll.
|
||||||
|
|
||||||
@ -652,3 +627,105 @@ class MotionPoll(CountInvalid, CountVotesCast, BasePoll):
|
|||||||
"""Apend the fields for invalid and votecast to the ModelForm."""
|
"""Apend the fields for invalid and votecast to the ModelForm."""
|
||||||
CountInvalid.append_pollform_fields(self, fields)
|
CountInvalid.append_pollform_fields(self, fields)
|
||||||
CountVotesCast.append_pollform_fields(self, fields)
|
CountVotesCast.append_pollform_fields(self, fields)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowError(OpenSlidesError):
|
||||||
|
"""Exception raised when errors in a workflow or state accure."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
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
|
from reportlab.lib import colors
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
openslides.motion.signales
|
openslides.motion.signals
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Signals for the motion app.
|
Signals for the motion app.
|
||||||
|
|
||||||
@ -26,5 +26,5 @@ def default_config(sender, key, **kwargs):
|
|||||||
'motion_pdf_ballot_papers_number': '8',
|
'motion_pdf_ballot_papers_number': '8',
|
||||||
'motion_pdf_title': _('Motions'),
|
'motion_pdf_title': _('Motions'),
|
||||||
'motion_pdf_preamble': '',
|
'motion_pdf_preamble': '',
|
||||||
'motion_create_new_version': 'ALLWASY_CREATE_NEW_VERSION',
|
'motion_allow_disable_versioning': False,
|
||||||
'motion_workflow': 'default'}.get(key)
|
'motion_workflow': 1}.get(key)
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
openslides.motion.slides
|
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.
|
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||||
:license: GNU GPL, see LICENSE for more details.
|
:license: GNU GPL, see LICENSE for more details.
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
<p>State: {{ motion.state }}</p>
|
<p>State: {{ motion.state }}</p>
|
||||||
<h4>possible stats:</h4>
|
<h4>possible stats:</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{% for state in motion.state.next_states %}
|
{% for state in motion.state.next_states.all %}
|
||||||
<li><a href="{% url 'motion_set_state' motion.pk state.id %}">{{ state }}</a></li>
|
<li><a href="{% url 'motion_set_state' motion.pk state.pk %}">{{ state }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li><a href="{% url 'motion_reset_state' motion.pk %}">Reset State</a></li>
|
<li><a href="{% url 'motion_reset_state' motion.pk %}">Reset State</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
from django.conf.urls import url, patterns
|
from django.conf.urls import url, patterns
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('openslides.motion.views',
|
urlpatterns = patterns('openslides.motion.views',
|
||||||
url(r'^$',
|
url(r'^$',
|
||||||
'motion_list',
|
'motion_list',
|
||||||
@ -78,7 +79,7 @@ urlpatterns = patterns('openslides.motion.views',
|
|||||||
name='motion_poll_delete',
|
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',
|
'set_state',
|
||||||
name='motion_set_state',
|
name='motion_set_state',
|
||||||
),
|
),
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
Views for the motion app.
|
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.
|
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||||
:license: GNU GPL, see LICENSE for more details.
|
: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.config.models import config
|
||||||
from openslides.agenda.models import Item
|
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,
|
from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin,
|
||||||
MotionCreateNewVersionMixin, ConfigForm)
|
MotionDisableVersioningMixin, ConfigForm)
|
||||||
from .workflow import WorkflowError
|
|
||||||
from .pdf import motions_to_pdf, motion_to_pdf
|
from .pdf import motions_to_pdf, motion_to_pdf
|
||||||
|
|
||||||
|
|
||||||
@ -81,7 +80,7 @@ motion_detail = MotionDetailView.as_view()
|
|||||||
|
|
||||||
|
|
||||||
class MotionMixin(object):
|
class MotionMixin(object):
|
||||||
"""Mixin for MotionViewsClasses, to save the version data."""
|
"""Mixin for MotionViewsClasses to save the version data."""
|
||||||
|
|
||||||
def manipulate_object(self, form):
|
def manipulate_object(self, form):
|
||||||
"""Save the version data into the motion object before it is saved in
|
"""Save the version data into the motion object before it is saved in
|
||||||
@ -125,8 +124,9 @@ class MotionMixin(object):
|
|||||||
form_classes.append(MotionSubmitterMixin)
|
form_classes.append(MotionSubmitterMixin)
|
||||||
if config['motion_min_supporters'] > 0:
|
if config['motion_min_supporters'] > 0:
|
||||||
form_classes.append(MotionSupporterMixin)
|
form_classes.append(MotionSupporterMixin)
|
||||||
if config['motion_create_new_version'] == 'ASK_USER':
|
if self.object:
|
||||||
form_classes.append(MotionCreateNewVersionMixin)
|
if config['motion_allow_disable_versioning'] and self.object.state.versioning:
|
||||||
|
form_classes.append(MotionDisableVersioningMixin)
|
||||||
return type('MotionForm', tuple(form_classes), {})
|
return type('MotionForm', tuple(form_classes), {})
|
||||||
|
|
||||||
|
|
||||||
@ -267,7 +267,7 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
|
|||||||
def case_yes(self):
|
def case_yes(self):
|
||||||
"""Append or remove the request.user from the motion.
|
"""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.
|
appending or removing the user.
|
||||||
"""
|
"""
|
||||||
if self.check_permission(self.request):
|
if self.check_permission(self.request):
|
||||||
@ -395,8 +395,8 @@ class MotionSetStateView(SingleObjectMixin, RedirectView):
|
|||||||
if self.reset:
|
if self.reset:
|
||||||
self.object.reset_state()
|
self.object.reset_state()
|
||||||
else:
|
else:
|
||||||
self.object.state = kwargs['state']
|
self.object.state = State.objects.get(pk=kwargs['state'])
|
||||||
except WorkflowError, e:
|
except WorkflowError, e: # TODO: Is a WorkflowError still possible here?
|
||||||
messages.error(request, e)
|
messages.error(request, e)
|
||||||
else:
|
else:
|
||||||
self.object.save()
|
self.object.save()
|
||||||
@ -484,7 +484,7 @@ class Config(FormView):
|
|||||||
'motion_pdf_ballot_papers_number': config['motion_pdf_ballot_papers_number'],
|
'motion_pdf_ballot_papers_number': config['motion_pdf_ballot_papers_number'],
|
||||||
'motion_pdf_title': config['motion_pdf_title'],
|
'motion_pdf_title': config['motion_pdf_title'],
|
||||||
'motion_pdf_preamble': config['motion_pdf_preamble'],
|
'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'],
|
'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_ballot_papers_number'] = form.cleaned_data['motion_pdf_ballot_papers_number']
|
||||||
config['motion_pdf_title'] = form.cleaned_data['motion_pdf_title']
|
config['motion_pdf_title'] = form.cleaned_data['motion_pdf_title']
|
||||||
config['motion_pdf_preamble'] = form.cleaned_data['motion_pdf_preamble']
|
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']
|
config['motion_workflow'] = form.cleaned_data['motion_workflow']
|
||||||
messages.success(self.request, _('Motion settings successfully saved.'))
|
messages.success(self.request, _('Motion settings successfully saved.'))
|
||||||
return super(Config, self).form_valid(form)
|
return super(Config, self).form_valid(form)
|
||||||
|
@ -1,144 +1,51 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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.
|
|
||||||
|
|
||||||
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
|
||||||
:license: GNU GPL, see LICENSE for more details.
|
: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 django.utils.translation import ugettext_noop
|
||||||
|
|
||||||
from openslides.config.models import config
|
from .models import Workflow, State
|
||||||
|
|
||||||
_workflow = None
|
|
||||||
|
|
||||||
|
|
||||||
class State(object):
|
def init_builtin_workflows():
|
||||||
"""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.
|
|
||||||
"""
|
|
||||||
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'.
|
|
||||||
"""
|
"""
|
||||||
for workflow in settings.MOTION_WORKFLOW:
|
Saves a simple and a complex workflow into the database. This function is only called manually.
|
||||||
yield workflow[0], workflow[1]
|
|
||||||
|
|
||||||
|
|
||||||
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
|
workflow_1 = Workflow(name=ugettext_noop('Simple Workflow'), id=1)
|
||||||
if _workflow is not None:
|
state_1_1 = State.objects.create(name=ugettext_noop('submitted'), workflow=workflow_1,
|
||||||
try:
|
allow_create_poll=True, allow_support=True, allow_submitter_edit=True)
|
||||||
return _workflow[state]
|
state_1_2 = State.objects.create(name=ugettext_noop('accepted'), workflow=workflow_1, action_word=ugettext_noop('accept'))
|
||||||
except KeyError:
|
state_1_3 = State.objects.create(name=ugettext_noop('rejected'), workflow=workflow_1, action_word=ugettext_noop('reject'))
|
||||||
raise WorkflowError('Unknown state: %s' % state)
|
state_1_4 = State.objects.create(name=ugettext_noop('not decided'), workflow=workflow_1, action_word=ugettext_noop('do not decide'))
|
||||||
_workflow = {}
|
state_1_1.next_states.add(state_1_2, state_1_3, state_1_4)
|
||||||
for workflow in settings.MOTION_WORKFLOW:
|
state_1_1.save() # Is this neccessary?
|
||||||
if workflow[0] == config['motion_workflow']:
|
workflow_1.first_state = state_1_1
|
||||||
try:
|
workflow_1.save()
|
||||||
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)
|
workflow_2 = Workflow(name=ugettext_noop('Complex Workflow'), id=2)
|
||||||
return get_state(state)
|
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)
|
||||||
def populate_workflow(state, workflow):
|
state_2_3 = State.objects.create(name=ugettext_noop('accepted'), workflow=workflow_2, action_word=ugettext_noop('accept'), versioning=True)
|
||||||
"""Append all 'next_states' from state to the workflow.
|
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,
|
||||||
The argument state has to be a state object.
|
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)
|
||||||
The argument workflow has to be a dictonary.
|
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,
|
||||||
Calls this function recrusiv with all next_states from the next_states states.
|
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)
|
||||||
workflow[state.id] = state
|
state_2_10 = State.objects.create(name=ugettext_noop('rejected (not authorized)'), workflow=workflow_2,
|
||||||
for s in state.next_states:
|
action_word=ugettext_noop('reject (not authorized)'), versioning=True)
|
||||||
if s.id not in workflow:
|
state_2_1.next_states.add(state_2_2, state_2_5, state_2_10)
|
||||||
populate_workflow(s, workflow)
|
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?
|
||||||
DUMMY_STATE = State('dummy', ugettext_noop('Unknwon state'))
|
workflow_2.first_state = state_2_1
|
||||||
"""A dummy state object. Returned, if the state_id is not known."""
|
workflow_2.save()
|
||||||
|
|
||||||
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)'))]
|
|
||||||
|
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,16 @@ from django.test import TestCase
|
|||||||
|
|
||||||
from openslides.participant.models import User
|
from openslides.participant.models import User
|
||||||
from openslides.config.models import config
|
from openslides.config.models import config
|
||||||
from openslides.motion.models import Motion
|
from openslides.motion.models import Motion, Workflow, State, WorkflowError
|
||||||
from openslides.motion.workflow import WorkflowError
|
|
||||||
|
|
||||||
|
|
||||||
class ModelTest(TestCase):
|
class ModelTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.motion = Motion.objects.create(title='v1')
|
self.motion = Motion.objects.create(title='v1')
|
||||||
self.test_user = User.objects.create(username='blub')
|
self.test_user = User.objects.create(username='blub')
|
||||||
|
self.workflow = Workflow.objects.get(pk=1)
|
||||||
|
|
||||||
def test_create_new_version(self):
|
def test_create_new_version(self):
|
||||||
config['motion_create_new_version'] = 'ALLWASY_CREATE_NEW_VERSION'
|
|
||||||
motion = Motion.objects.create(title='m1')
|
motion = Motion.objects.create(title='m1')
|
||||||
self.assertEqual(motion.versions.count(), 1)
|
self.assertEqual(motion.versions.count(), 1)
|
||||||
|
|
||||||
@ -32,13 +31,16 @@ class ModelTest(TestCase):
|
|||||||
|
|
||||||
motion.title = 'new title'
|
motion.title = 'new title'
|
||||||
motion.save()
|
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()
|
motion.save()
|
||||||
self.assertEqual(motion.versions.count(), 3)
|
self.assertEqual(motion.versions.count(), 3)
|
||||||
|
|
||||||
config['motion_create_new_version'] = 'NEVER_CREATE_NEW_VERSION'
|
|
||||||
motion.text = 'new text'
|
|
||||||
motion.save()
|
motion.save()
|
||||||
self.assertEqual(motion.versions.count(), 3)
|
self.assertEqual(motion.versions.count(), 3)
|
||||||
|
|
||||||
@ -56,6 +58,7 @@ class ModelTest(TestCase):
|
|||||||
|
|
||||||
def test_version(self):
|
def test_version(self):
|
||||||
motion = Motion.objects.create(title='v1')
|
motion = Motion.objects.create(title='v1')
|
||||||
|
motion.state = State.objects.create(name='automatic_versioning', workflow=self.workflow, versioning=True)
|
||||||
motion.title = 'v2'
|
motion.title = 'v2'
|
||||||
motion.save()
|
motion.save()
|
||||||
v2_version = motion.version
|
v2_version = motion.version
|
||||||
@ -94,24 +97,42 @@ class ModelTest(TestCase):
|
|||||||
self.motion.unsupport(self.test_user)
|
self.motion.unsupport(self.test_user)
|
||||||
|
|
||||||
def test_poll(self):
|
def test_poll(self):
|
||||||
self.motion.state = 'per'
|
self.motion.state = State.objects.get(pk=1)
|
||||||
poll = self.motion.create_poll()
|
poll = self.motion.create_poll()
|
||||||
self.assertEqual(poll.poll_number, 1)
|
self.assertEqual(poll.poll_number, 1)
|
||||||
|
|
||||||
def test_state(self):
|
def test_state(self):
|
||||||
self.motion.reset_state()
|
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):
|
with self.assertRaises(WorkflowError):
|
||||||
self.motion.create_poll()
|
self.motion.create_poll()
|
||||||
|
|
||||||
self.motion.set_state('per')
|
self.motion.state = State.objects.get(pk=6)
|
||||||
self.assertEqual(self.motion.state.id, 'per')
|
self.assertEqual(self.motion.state.name, 'permitted')
|
||||||
|
self.assertEqual(self.motion.state.get_action_word(), 'permit')
|
||||||
with self.assertRaises(WorkflowError):
|
with self.assertRaises(WorkflowError):
|
||||||
self.motion.support(self.test_user)
|
self.motion.support(self.test_user)
|
||||||
with self.assertRaises(WorkflowError):
|
with self.assertRaises(WorkflowError):
|
||||||
self.motion.unsupport(self.test_user)
|
self.motion.unsupport(self.test_user)
|
||||||
|
|
||||||
with self.assertRaises(WorkflowError):
|
def test_new_states_or_workflows(self):
|
||||||
self.motion.set_state('per')
|
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