diff --git a/openslides/global_settings.py b/openslides/global_settings.py index 94f900059..a02ef47e2 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -26,9 +26,12 @@ LOGIN_REDIRECT_URL = '/' 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')), diff --git a/openslides/motion/forms.py b/openslides/motion/forms.py index 66307761a..b488a7a2a 100644 --- a/openslides/motion/forms.py +++ b/openslides/motion/forms.py @@ -16,6 +16,7 @@ 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 class BaseMotionForm(forms.ModelForm, CssClassMixin): @@ -117,3 +118,9 @@ class ConfigForm(forms.Form, CssClassMixin): ('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"), + required=True, + choices=motion_workflow_choices()) diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 8c8d8ad60..323bb153c 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -30,6 +30,8 @@ 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 + # TODO: Save submitter and supporter in the same table class MotionSubmitter(models.Model): @@ -58,8 +60,7 @@ class Motion(SlideMixin, models.Model): # is deactivated. Maybe it has to be renamed. permitted_version = models.ForeignKey( 'MotionVersion', null=True, blank=True, related_name="permitted") - # TODO: Define status - status = models.CharField(max_length=3) + state_id = models.CharField(max_length=3) # Log (Translatable) identifier = models.CharField(max_length=255, null=True, blank=True, unique=True) @@ -86,6 +87,9 @@ class Motion(SlideMixin, models.Model): """ Saves the motion. Create or update a motion_version object """ + if not self.state_id: + self.state = 'default' + super(Motion, self).save(*args, **kwargs) for attr in ['title', 'text', 'reason']: if getattr(self, attr) != getattr(self.last_version, attr): @@ -268,12 +272,33 @@ class Motion(SlideMixin, models.Model): #self.writelog(_("Supporter: -%s") % (person)) def create_poll(self): + """ + Create a new poll for this motion + """ # 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 + def get_state(self): + """ + Get the state of this motion. Return a State object. + """ + return get_state(self.state_id) + + def set_state(self, state): + """ + Set the state of this motion. + + state has to be a valid state id or State object. + """ + if type(state) is not State: + state = get_state(state) + self.state_id = state.id + + state = property(get_state, set_state) + class MotionVersion(models.Model): title = models.CharField(max_length=255, verbose_name=ugettext_lazy("Title")) diff --git a/openslides/motion/signals.py b/openslides/motion/signals.py index c9b1ece36..5735545a8 100644 --- a/openslides/motion/signals.py +++ b/openslides/motion/signals.py @@ -25,4 +25,4 @@ def default_config(sender, key, **kwargs): 'motion_pdf_title': _('Motions'), 'motion_pdf_preamble': '', 'motion_create_new_version': 'ALLWASY_CREATE_NEW_VERSION', - }.get(key) + 'motion_workflow': 'default'}.get(key) diff --git a/openslides/motion/templates/motion/motion_detail.html b/openslides/motion/templates/motion/motion_detail.html index 22e80067c..8a498a6c5 100644 --- a/openslides/motion/templates/motion/motion_detail.html +++ b/openslides/motion/templates/motion/motion_detail.html @@ -12,6 +12,7 @@

Reason: {{ motion.reason }}

Submitter: {% for submitter in motion.submitter.all %}{{ submitter.person }} {% endfor %}

Supporter: {% for supporter in motion.supporter.all %}{{ supporter.person }} {% endfor %}

+

State: {{ motion.state }}

    {% for motion_version in motion.versions.all %} diff --git a/openslides/motion/views.py b/openslides/motion/views.py index b08f6d29f..ff627880b 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -248,6 +248,7 @@ class Config(FormView): 'motion_pdf_title': config['motion_pdf_title'], 'motion_pdf_preamble': config['motion_pdf_preamble'], 'motion_create_new_version': config['motion_create_new_version'], + 'motion_workflow': config['motion_workflow'], } def form_valid(self, form): @@ -258,6 +259,7 @@ class Config(FormView): 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_workflow'] = form.cleaned_data['motion_workflow'] messages.success(self.request, _('Motion settings successfully saved.')) return super(Config, self).form_valid(form) diff --git a/openslides/motion/workflow.py b/openslides/motion/workflow.py new file mode 100644 index 000000000..bb4c809c3 --- /dev/null +++ b/openslides/motion/workflow.py @@ -0,0 +1,74 @@ +from django.conf import settings +from django.core import exceptions +from django.utils.importlib import import_module + +from openslides.config.models import config +ugettext = lambda s: s + +_workflow = None + +class State(object): + def __init__(self, id, name, next_states=[], poll=False, support=False): + self.id = id + self.name = name + self.next_states = next_states + self.poll = poll + self.support = support + + def __unicode__(self): + return self.name + + +def motion_workflow_choices(): + for workflow in settings.MOTION_WORKFLOW: + yield workflow[0], workflow[1] + + +def get_state(state='default'): + global _workflow + if _workflow is not None: + return _workflow[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): + workflow[state.id] = state + for s in state.next_states: + if s.id not in workflow: + populate_workflow(s, workflow) + + +default_workflow = State('pub', ugettext('Published'), support=True, next_states=[ + State('per', ugettext('Permitted'), poll=True, next_states=[ + State('acc', ugettext('Accepted')), + State('rej', ugettext('Rejected')), + State('wit', ugettext('Withdrawed')), + State('adj', ugettext('Adjourned')), + State('noc', ugettext('Not Concerned')), + State('com', ugettext('Commited a bill')), + State('rev', ugettext('Needs Review'))]), + State('nop', ugettext('Rejected (not authorized)'))])