diff --git a/openslides/motion/__init__.py b/openslides/motion/__init__.py index ea23517a7..e296be3ce 100644 --- a/openslides/motion/__init__.py +++ b/openslides/motion/__init__.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- """ openslides.motion diff --git a/openslides/motion/forms.py b/openslides/motion/forms.py index 139d4205e..7b81d6524 100644 --- a/openslides/motion/forms.py +++ b/openslides/motion/forms.py @@ -15,7 +15,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, Workflow +from .models import Motion, Workflow, Category class BaseMotionForm(forms.ModelForm, CssClassMixin): @@ -90,6 +90,18 @@ class MotionDisableVersioningMixin(forms.ModelForm): last_version will be used.""" +class MotionCategoryMixin(forms.ModelForm): + """Mixin to let the user choose the category for the motion.""" + + category = forms.ModelChoiceField(queryset=Category.objects.all(), required=False) + + +class MotionIdentifierMixin(forms.ModelForm): + """Mixin to let the user choose the identifier for the motion.""" + + identifier = forms.CharField(required=False) + + class ConfigForm(CssClassMixin, forms.Form): """Form for the configuration tab of OpenSlides.""" motion_min_supporters = forms.IntegerField( diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 989fe6857..e4cf800b4 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -16,7 +16,7 @@ from datetime import datetime from django.core.urlresolvers import reverse -from django.db import models +from django.db import models, IntegrityError from django.db.models import Max from django.dispatch import receiver from django.utils import formats @@ -37,6 +37,10 @@ from openslides.agenda.models import Item from .exceptions import MotionError, WorkflowError +# TODO: into the config-tab +config['motion_identifier'] = ('manually', 'per_category', 'serially_numbered')[2] + + class Motion(SlideMixin, models.Model): """The Motion Class. @@ -65,7 +69,15 @@ class Motion(SlideMixin, models.Model): unique=True) """A string as human readable identifier for the motion.""" - # category = models.ForeignKey('Category', null=True, blank=True) + identifier_number = models.IntegerField(null=True) + """Counts the number of the motion in one category. + + Needed to find the next free motion-identifier. + """ + + category = models.ForeignKey('Category', null=True, blank=True) + """ForeignKey to one category of motions.""" + # TODO: proposal #master = models.ForeignKey('self', null=True, blank=True) @@ -168,6 +180,33 @@ class Motion(SlideMixin, models.Model): if link == 'delete': return reverse('motion_delete', args=[str(self.id)]) + def set_identifier(self): + if config['motion_identifier'] == 'manually': + # Do not set an identifier. + return + elif config['motion_identifier'] == 'per_category': + motions = Motion.objects.filter(category=self.category) + else: + motions = Motion.objects.all() + + number = motions.aggregate(Max('identifier_number'))['identifier_number__max'] or 0 + if self.category is None or not self.category.prefix: + prefix = '' + else: + prefix = self.category.prefix + ' ' + + while True: + number += 1 + self.identifier = '%s%d' % (prefix, number) + try: + self.save() + except IntegrityError: + continue + else: + self.number = number + self.save() + break + def get_title(self): """Get the title of the motion. @@ -344,6 +383,18 @@ class Motion(SlideMixin, models.Model): else: raise WorkflowError('You can not create a poll in state %s.' % self.state.name) + def set_state(self, state): + """Set the state of the motion. + + State can be the id of a state object or a state object. + """ + if type(state) is int: + state = State.objects.get(pk=state) + + if not state.dont_set_identifier: + self.set_identifier() + self.state = state + def reset_state(self): """Set the state to the default state. If the motion is new, it chooses the default workflow from config.""" if self.state: @@ -524,12 +575,24 @@ class MotionSupporter(models.Model): return unicode(self.person) -## class Category(models.Model): - ## name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name")) - ## prefix = models.CharField(max_length=32, verbose_name=ugettext_lazy("Category prefix")) +class Category(models.Model): + name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name")) + """Name of the category.""" - ## def __unicode__(self): - ## return self.name + prefix = models.CharField(blank=True, max_length=32, verbose_name=ugettext_lazy("Category prefix")) + """Prefix of the category. + + Used to build the identifier of a motion. + """ + + def __unicode__(self): + return self.name + + def get_absolute_url(self, link='update'): + if link == 'update' or link == 'edit': + return reverse('motion_category_update', args=[str(self.id)]) + if link == 'delete': + return reverse('motion_category_delete', args=[str(self.id)]) ## class Comment(models.Model): @@ -685,6 +748,12 @@ class State(models.Model): dont_set_new_version_active = models.BooleanField(default=False) """If true, new versions are not automaticly set active.""" + dont_set_identifier = models.BooleanField(default=False) + """Decides if the motion gets an identifier. + + If true, the motion does not get an identifier if the state change to + this one, else it does.""" + def __unicode__(self): """Returns the name of the state.""" return self.name diff --git a/openslides/motion/signals.py b/openslides/motion/signals.py index 526a56783..ff662ddf1 100644 --- a/openslides/motion/signals.py +++ b/openslides/motion/signals.py @@ -61,7 +61,8 @@ def create_builtin_workflows(sender, **kwargs): state_2_1 = State.objects.create(name=ugettext_noop('published'), workflow=workflow_2, allow_support=True, - allow_submitter_edit=True) + allow_submitter_edit=True, + dont_set_identifier=True) state_2_2 = State.objects.create(name=ugettext_noop('permitted'), workflow=workflow_2, action_word=ugettext_noop('Permit'), diff --git a/openslides/motion/templates/motion/category_form.html b/openslides/motion/templates/motion/category_form.html new file mode 100644 index 000000000..5114c9a0a --- /dev/null +++ b/openslides/motion/templates/motion/category_form.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% load tags %} +{% load i18n %} + +{% block title %} + {{ block.super }} – + {% if motion %} + {% trans "Edit category" %} + {% else %} + {% trans "New category" %} + {% endif %} +{% endblock %} + +{% block content %} +

+ {% if motion %} + {% trans "Edit category" %} + {% else %} + {% trans "New category" %} + {% endif %} +

+
{% csrf_token %} + {% include "form.html" %} +

+ {% include "formbuttons_saveapply.html" %} + + {% trans 'Cancel' %} + +

+ * {% trans "required" %} +
+{% endblock %} diff --git a/openslides/motion/templates/motion/category_list.html b/openslides/motion/templates/motion/category_list.html new file mode 100644 index 000000000..175cf2854 --- /dev/null +++ b/openslides/motion/templates/motion/category_list.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% load tags %} +{% load i18n %} + +{% block title %}{{ block.super }} – {% trans "Motions" %}{% endblock %} + +{% block content %} +

{% trans "Categories" %}

+ {% for category in category_list %} +

{{ category }}

+ {% empty %} +

No Categories

+ {% endfor %} + +{% endblock %} diff --git a/openslides/motion/templates/motion/motion_detail.html b/openslides/motion/templates/motion/motion_detail.html index a9de961b6..12d26fba3 100644 --- a/openslides/motion/templates/motion/motion_detail.html +++ b/openslides/motion/templates/motion/motion_detail.html @@ -8,11 +8,11 @@ {% block content %}

- {{ motion.title }} + {{ motion.title }} {{ motion.category }}
- {% if motion.number != None %} - {% trans "Motion" %} {{ motion.number }}, + {% if motion.identifier != None %} + {% trans "Motion" %} {{ motion.identifier }}, {% else %} [{% trans "no number" %}], {% endif %} diff --git a/openslides/motion/urls.py b/openslides/motion/urls.py index a2b6b49e2..85de6fbfc 100644 --- a/openslides/motion/urls.py +++ b/openslides/motion/urls.py @@ -19,8 +19,9 @@ urlpatterns = patterns('openslides.motion.views', name='motion_list', ), - url(r'^create/$', + url(r'^new/$', 'motion_create', + # TODO: rename to motion_create name='motion_new', ), @@ -39,6 +40,11 @@ urlpatterns = patterns('openslides.motion.views', name='motion_delete', ), + url(r'^(?P\d+)/set_identifier/', + 'set_identifier', + name='motion_set_identifier', + ), + url(r'^(?P\d+)/version/(?P\d+)/$', 'motion_detail', name='motion_version_detail', @@ -103,4 +109,24 @@ urlpatterns = patterns('openslides.motion.views', 'motion_detail_pdf', name='motion_detail_pdf', ), + + url(r'^category/$', + 'category_list', + name='motion_category_list', + ), + + url(r'^category/new/$', + 'category_create', + name='motion_category_create', + ), + + url(r'^category/(?P\d+)/edit/$', + 'category_update', + name='motion_category_update', + ), + + url(r'^category/(?P\d+)/del/$', + 'category_delete', + name='motion_category_delete', + ), ) diff --git a/openslides/motion/views.py b/openslides/motion/views.py index da7d27dd4..d397f8b78 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -32,9 +32,11 @@ 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, State, WorkflowError +from .models import (Motion, MotionSubmitter, MotionSupporter, MotionPoll, + MotionVersion, State, WorkflowError, Category) from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin, - MotionDisableVersioningMixin, ConfigForm) + MotionDisableVersioningMixin, ConfigForm, MotionCategoryMixin, + MotionIdentifierMixin) from .pdf import motions_to_pdf, motion_to_pdf @@ -97,6 +99,16 @@ class MotionMixin(object): except KeyError: pass + try: + self.object.category = form.cleaned_data['category'] + except KeyError: + pass + + try: + self.object.identifier = form.cleaned_data['identifier'] + except KeyError: + pass + def post_save(self, form): """Save the submitter an the supporter so the motion.""" super(MotionMixin, self).post_save(form) @@ -119,10 +131,17 @@ class MotionMixin(object): will be mixed in dependence of some config values. See motion.forms for more information on the mixins. """ + form_classes = [] + + if (self.request.user.has_perm('motion.can_manage_motion') and + config['motion_identifier'] == 'manually'): + form_classes.append(MotionIdentifierMixin) + + form_classes.append(BaseMotionForm) - form_classes = [BaseMotionForm] if self.request.user.has_perm('motion.can_manage_motion'): form_classes.append(MotionSubmitterMixin) + form_classes.append(MotionCategoryMixin) if config['motion_min_supporters'] > 0: form_classes.append(MotionSupporterMixin) if self.object: @@ -228,6 +247,30 @@ class VersionRejectView(GetVersionMixin, SingleObjectMixin, QuestionMixin, Redir version_reject = VersionRejectView.as_view() +class SetIdentifierView(SingleObjectMixin, RedirectView): + """Set the identifier of the motion. + + See motion.set_identifier for more informations + """ + permission_required = 'motion.can_manage_motion' + model = Motion + url_name = 'motion_detail' + + def get(self, request, *args, **kwargs): + """Set self.object to a motion.""" + self.object = self.get_object() + return super(SetIdentifierView, self).get(request, *args, **kwargs) + + def pre_redirect(self, request, *args, **kwargs): + """Set the identifier.""" + self.object.set_identifier() + + def get_url_name_args(self): + return [self.object.id] + +set_identifier = SetIdentifierView.as_view() + + class SupportView(SingleObjectMixin, QuestionMixin, RedirectView): """View to support or unsupport a motion. @@ -399,7 +442,7 @@ class MotionSetStateView(SingleObjectMixin, RedirectView): if self.reset: self.object.reset_state() else: - self.object.state = State.objects.get(pk=kwargs['state']) + self.object.set_state(int(kwargs['state'])) except WorkflowError, e: # TODO: Is a WorkflowError still possible here? messages.error(request, e) else: @@ -473,6 +516,35 @@ motion_list_pdf = MotionPDFView.as_view(print_all_motions=True) motion_detail_pdf = MotionPDFView.as_view(print_all_motions=False) +class CategoryListView(ListView): + permission_required = 'motion.can_manage_motion' + model = Category + +category_list = CategoryListView.as_view() + + +class CategoryCreateView(CreateView): + permission_required = 'motion.can_manage_motion' + model = Category + +category_create = CategoryCreateView.as_view() + + +class CategoryUpdateView(UpdateView): + permission_required = 'motion.can_manage_motion' + model = Category + +category_update = CategoryUpdateView.as_view() + + +class CategoryDeleteView(DeleteView): + permission_required = 'motion.can_manage_motion' + model = Category + success_url_name = 'motion_category_list' + +category_delete = CategoryDeleteView.as_view() + + class Config(FormView): """The View for the config tab.""" permission_required = 'config.can_manage_config' diff --git a/openslides/utils/views.py b/openslides/utils/views.py index 928e56767..00bc6b9fa 100644 --- a/openslides/utils/views.py +++ b/openslides/utils/views.py @@ -304,7 +304,8 @@ class DeleteView(SingleObjectMixin, QuestionMixin, RedirectView): return super(DeleteView, self).get(request, *args, **kwargs) def get_redirect_url(self, **kwargs): - if self.request.method == 'GET' and self.question_url_name is None: + if self.question_url_name is None and (self.request.method == 'GET' or + self.get_answer() == 'no'): return self.object.get_absolute_url() else: return super(DeleteView, self).get_redirect_url(**kwargs)