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 -*-
"""
openslides.motion

View File

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

View File

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

View File

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

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 %}
<h1>
{{ motion.title }}
{{ motion.title }} {{ motion.category }}
<br>
<small>
{% if motion.number != None %}
{% trans "Motion" %} {{ motion.number }},
{% if motion.identifier != None %}
{% trans "Motion" %} {{ motion.identifier }},
{% else %}
<i>[{% trans "no number" %}]</i>,
{% endif %}

View File

@ -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<pk>\d+)/set_identifier/',
'set_identifier',
name='motion_set_identifier',
),
url(r'^(?P<pk>\d+)/version/(?P<version_number>\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<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.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'

View File

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