Merge pull request #563 from ostcar/category

Category
This commit is contained in:
Oskar Hahn 2013-03-12 15:56:18 -07:00
commit 2a33210276
10 changed files with 249 additions and 18 deletions

View File

@ -1,3 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
openslides.motion openslides.motion

View File

@ -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(

View File

@ -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

View File

@ -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'),

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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',
),
) )

View File

@ -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'

View File

@ -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)