commit
2a33210276
|
@ -1,3 +1,4 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
openslides.motion
|
openslides.motion
|
||||||
|
|
|
@ -15,7 +15,7 @@ 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, Workflow
|
from .models import Motion, Workflow, Category
|
||||||
|
|
||||||
|
|
||||||
class BaseMotionForm(forms.ModelForm, CssClassMixin):
|
class BaseMotionForm(forms.ModelForm, CssClassMixin):
|
||||||
|
@ -90,6 +90,18 @@ class MotionDisableVersioningMixin(forms.ModelForm):
|
||||||
last_version will be used."""
|
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):
|
class ConfigForm(CssClassMixin, forms.Form):
|
||||||
"""Form for the configuration tab of OpenSlides."""
|
"""Form for the configuration tab of OpenSlides."""
|
||||||
motion_min_supporters = forms.IntegerField(
|
motion_min_supporters = forms.IntegerField(
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
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.db.models import Max
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
|
@ -37,6 +37,10 @@ from openslides.agenda.models import Item
|
||||||
from .exceptions import MotionError, WorkflowError
|
from .exceptions import MotionError, WorkflowError
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: into the config-tab
|
||||||
|
config['motion_identifier'] = ('manually', 'per_category', 'serially_numbered')[2]
|
||||||
|
|
||||||
|
|
||||||
class Motion(SlideMixin, models.Model):
|
class Motion(SlideMixin, models.Model):
|
||||||
"""The Motion Class.
|
"""The Motion Class.
|
||||||
|
|
||||||
|
@ -65,7 +69,15 @@ class Motion(SlideMixin, models.Model):
|
||||||
unique=True)
|
unique=True)
|
||||||
"""A string as human readable identifier for the motion."""
|
"""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
|
# TODO: proposal
|
||||||
#master = models.ForeignKey('self', null=True, blank=True)
|
#master = models.ForeignKey('self', null=True, blank=True)
|
||||||
|
|
||||||
|
@ -168,6 +180,33 @@ class Motion(SlideMixin, models.Model):
|
||||||
if link == 'delete':
|
if link == 'delete':
|
||||||
return reverse('motion_delete', args=[str(self.id)])
|
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):
|
def get_title(self):
|
||||||
"""Get the title of the motion.
|
"""Get the title of the motion.
|
||||||
|
|
||||||
|
@ -344,6 +383,18 @@ class Motion(SlideMixin, models.Model):
|
||||||
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 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):
|
def reset_state(self):
|
||||||
"""Set the state to the default state. If the motion is new, it chooses the default workflow from config."""
|
"""Set the state to the default state. If the motion is new, it chooses the default workflow from config."""
|
||||||
if self.state:
|
if self.state:
|
||||||
|
@ -524,12 +575,24 @@ class MotionSupporter(models.Model):
|
||||||
return unicode(self.person)
|
return unicode(self.person)
|
||||||
|
|
||||||
|
|
||||||
## class Category(models.Model):
|
class Category(models.Model):
|
||||||
## name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name"))
|
name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name"))
|
||||||
## prefix = models.CharField(max_length=32, verbose_name=ugettext_lazy("Category prefix"))
|
"""Name of the category."""
|
||||||
|
|
||||||
## def __unicode__(self):
|
prefix = models.CharField(blank=True, max_length=32, verbose_name=ugettext_lazy("Category prefix"))
|
||||||
## return self.name
|
"""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):
|
## class Comment(models.Model):
|
||||||
|
@ -685,6 +748,12 @@ class State(models.Model):
|
||||||
dont_set_new_version_active = models.BooleanField(default=False)
|
dont_set_new_version_active = models.BooleanField(default=False)
|
||||||
"""If true, new versions are not automaticly set active."""
|
"""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):
|
def __unicode__(self):
|
||||||
"""Returns the name of the state."""
|
"""Returns the name of the state."""
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
@ -61,7 +61,8 @@ def create_builtin_workflows(sender, **kwargs):
|
||||||
state_2_1 = State.objects.create(name=ugettext_noop('published'),
|
state_2_1 = State.objects.create(name=ugettext_noop('published'),
|
||||||
workflow=workflow_2,
|
workflow=workflow_2,
|
||||||
allow_support=True,
|
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'),
|
state_2_2 = State.objects.create(name=ugettext_noop('permitted'),
|
||||||
workflow=workflow_2,
|
workflow=workflow_2,
|
||||||
action_word=ugettext_noop('Permit'),
|
action_word=ugettext_noop('Permit'),
|
||||||
|
|
|
@ -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 %}
|
||||||
|
<h1>
|
||||||
|
{% if motion %}
|
||||||
|
{% trans "Edit category" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "New category" %}
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
<form action="" method="post">{% csrf_token %}
|
||||||
|
{% include "form.html" %}
|
||||||
|
<p>
|
||||||
|
{% include "formbuttons_saveapply.html" %}
|
||||||
|
<a href='{% url 'motion_list' %}' class="btn">
|
||||||
|
{% trans 'Cancel' %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<small>* {% trans "required" %}</small>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load tags %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{{ block.super }} – {% trans "Motions" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Categories" %}</h1>
|
||||||
|
{% for category in category_list %}
|
||||||
|
<p><a href="{% model_url category 'update' %}">{{ category }}</a></p>
|
||||||
|
{% empty %}
|
||||||
|
<p>No Categories</p>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
|
@ -8,11 +8,11 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>
|
<h1>
|
||||||
{{ motion.title }}
|
{{ motion.title }} {{ motion.category }}
|
||||||
<br>
|
<br>
|
||||||
<small>
|
<small>
|
||||||
{% if motion.number != None %}
|
{% if motion.identifier != None %}
|
||||||
{% trans "Motion" %} {{ motion.number }},
|
{% trans "Motion" %} {{ motion.identifier }},
|
||||||
{% else %}
|
{% else %}
|
||||||
<i>[{% trans "no number" %}]</i>,
|
<i>[{% trans "no number" %}]</i>,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -19,8 +19,9 @@ urlpatterns = patterns('openslides.motion.views',
|
||||||
name='motion_list',
|
name='motion_list',
|
||||||
),
|
),
|
||||||
|
|
||||||
url(r'^create/$',
|
url(r'^new/$',
|
||||||
'motion_create',
|
'motion_create',
|
||||||
|
# TODO: rename to motion_create
|
||||||
name='motion_new',
|
name='motion_new',
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -39,6 +40,11 @@ urlpatterns = patterns('openslides.motion.views',
|
||||||
name='motion_delete',
|
name='motion_delete',
|
||||||
),
|
),
|
||||||
|
|
||||||
|
url(r'^(?P<pk>\d+)/set_identifier/',
|
||||||
|
'set_identifier',
|
||||||
|
name='motion_set_identifier',
|
||||||
|
),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/$',
|
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/$',
|
||||||
'motion_detail',
|
'motion_detail',
|
||||||
name='motion_version_detail',
|
name='motion_version_detail',
|
||||||
|
@ -103,4 +109,24 @@ urlpatterns = patterns('openslides.motion.views',
|
||||||
'motion_detail_pdf',
|
'motion_detail_pdf',
|
||||||
name='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<pk>\d+)/edit/$',
|
||||||
|
'category_update',
|
||||||
|
name='motion_category_update',
|
||||||
|
),
|
||||||
|
|
||||||
|
url(r'^category/(?P<pk>\d+)/del/$',
|
||||||
|
'category_delete',
|
||||||
|
name='motion_category_delete',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,9 +32,11 @@ 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, State, WorkflowError
|
from .models import (Motion, MotionSubmitter, MotionSupporter, MotionPoll,
|
||||||
|
MotionVersion, State, WorkflowError, Category)
|
||||||
from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin,
|
from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin,
|
||||||
MotionDisableVersioningMixin, ConfigForm)
|
MotionDisableVersioningMixin, ConfigForm, MotionCategoryMixin,
|
||||||
|
MotionIdentifierMixin)
|
||||||
from .pdf import motions_to_pdf, motion_to_pdf
|
from .pdf import motions_to_pdf, motion_to_pdf
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,6 +99,16 @@ class MotionMixin(object):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
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):
|
def post_save(self, form):
|
||||||
"""Save the submitter an the supporter so the motion."""
|
"""Save the submitter an the supporter so the motion."""
|
||||||
super(MotionMixin, self).post_save(form)
|
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
|
will be mixed in dependence of some config values. See motion.forms
|
||||||
for more information on the mixins.
|
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'):
|
if self.request.user.has_perm('motion.can_manage_motion'):
|
||||||
form_classes.append(MotionSubmitterMixin)
|
form_classes.append(MotionSubmitterMixin)
|
||||||
|
form_classes.append(MotionCategoryMixin)
|
||||||
if config['motion_min_supporters'] > 0:
|
if config['motion_min_supporters'] > 0:
|
||||||
form_classes.append(MotionSupporterMixin)
|
form_classes.append(MotionSupporterMixin)
|
||||||
if self.object:
|
if self.object:
|
||||||
|
@ -228,6 +247,30 @@ class VersionRejectView(GetVersionMixin, SingleObjectMixin, QuestionMixin, Redir
|
||||||
version_reject = VersionRejectView.as_view()
|
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):
|
class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
|
||||||
"""View to support or unsupport a motion.
|
"""View to support or unsupport a motion.
|
||||||
|
|
||||||
|
@ -399,7 +442,7 @@ class MotionSetStateView(SingleObjectMixin, RedirectView):
|
||||||
if self.reset:
|
if self.reset:
|
||||||
self.object.reset_state()
|
self.object.reset_state()
|
||||||
else:
|
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?
|
except WorkflowError, e: # TODO: Is a WorkflowError still possible here?
|
||||||
messages.error(request, e)
|
messages.error(request, e)
|
||||||
else:
|
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)
|
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):
|
class Config(FormView):
|
||||||
"""The View for the config tab."""
|
"""The View for the config tab."""
|
||||||
permission_required = 'config.can_manage_config'
|
permission_required = 'config.can_manage_config'
|
||||||
|
|
|
@ -304,7 +304,8 @@ class DeleteView(SingleObjectMixin, QuestionMixin, RedirectView):
|
||||||
return super(DeleteView, self).get(request, *args, **kwargs)
|
return super(DeleteView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_redirect_url(self, **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()
|
return self.object.get_absolute_url()
|
||||||
else:
|
else:
|
||||||
return super(DeleteView, self).get_redirect_url(**kwargs)
|
return super(DeleteView, self).get_redirect_url(**kwargs)
|
||||||
|
|
Loading…
Reference in New Issue