Wrote docstrings to the motion app

This commit is contained in:
Oskar Hahn 2013-02-05 18:46:46 +01:00
parent a34731c00e
commit a08cf84ab8
9 changed files with 500 additions and 173 deletions

View File

@ -1,2 +1,14 @@
# -*- coding: utf-8 -*-
"""
openslides.motion
~~~~~~~~~~~~~~~~~
The OpenSlides motion app appends the functionality to OpenSlides, to
manage motions.
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
import openslides.motion.signals
import openslides.motion.slides

View File

@ -2,11 +2,11 @@
# -*- coding: utf-8 -*-
"""
openslides.motion.forms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~
Forms for the motion app.
Defines the DjangoForms for the motion app.
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS.
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
@ -20,14 +20,30 @@ from .workflow import motion_workflow_choices
class BaseMotionForm(forms.ModelForm, CssClassMixin):
"""Base FormClass for a Motion.
For it's own, it append the version data es fields.
The Class can be mixed with the following Mixins to add fields for the
submitter, supporters etc.
"""
Form to automaticly save the version data for a motion.
"""
title = forms.CharField(widget=forms.TextInput(), label=_("Title"))
"""Title of the Motion. Will be saved in a MotionVersion object."""
text = forms.CharField(widget=forms.Textarea(), label=_("Text"))
"""Text of the Motion. Will be saved in a MotionVersion object."""
reason = forms.CharField(
widget=forms.Textarea(), required=False, label=_("Reason"))
"""Reason of the Motion. will be saved in a MotionVersion object."""
class Meta:
model = Motion
fields = ()
def __init__(self, *args, **kwargs):
"""Fill the FormFields releated to the version data with initial data."""
self.motion = kwargs.get('instance', None)
self.initial = kwargs.setdefault('initial', {})
if self.motion is not None:
@ -36,16 +52,15 @@ class BaseMotionForm(forms.ModelForm, CssClassMixin):
self.initial['reason'] = self.motion.reason
super(BaseMotionForm, self).__init__(*args, **kwargs)
title = forms.CharField(widget=forms.TextInput(), label=_("Title"))
text = forms.CharField(widget=forms.Textarea(), label=_("Text"))
reason = forms.CharField(
widget=forms.Textarea(), required=False, label=_("Reason"))
class MotionSubmitterMixin(forms.ModelForm):
"""Mixin to append the submitter field to a MotionForm."""
submitter = MultiplePersonFormField(label=_("Submitter"))
"""Submitter of the Motion. Can be one or more persons."""
def __init__(self, *args, **kwargs):
"""Fill in the submitter of the motion as default value."""
if self.motion is not None:
submitter = [submitter.person.person_id for submitter in self.motion.submitter.all()]
self.initial['submitter'] = submitter
@ -53,9 +68,13 @@ class MotionSubmitterMixin(forms.ModelForm):
class MotionSupporterMixin(forms.ModelForm):
"""Mixin to append the supporter field to a Motionform."""
supporter = MultiplePersonFormField(required=False, label=_("Supporters"))
"""Supporter of the Motion. Can be one or more persons."""
def __init__(self, *args, **kwargs):
"""Fill in the supporter of the motions as default value."""
if self.motion is not None:
supporter = [supporter.person.person_id for supporter in self.motion.supporter.all()]
self.initial['supporter'] = supporter
@ -63,18 +82,21 @@ class MotionSupporterMixin(forms.ModelForm):
class MotionCreateNewVersionMixin(forms.ModelForm):
"""Mixin to add the option to the form, to choose, to create a new version."""
new_version = forms.BooleanField(
required=False, label=_("Create new version"), initial=True,
help_text=_("Trivial changes don't create a new version."))
"""BooleanField to decide, if a new version will be created, or the
last_version will be used."""
class ConfigForm(forms.Form, CssClassMixin):
class ConfigForm(CssClassMixin, forms.Form):
"""Form for the configuration tab of OpenSlides."""
motion_min_supporters = forms.IntegerField(
widget=forms.TextInput(attrs={'class': 'small-input'}),
label=_("Number of (minimum) required supporters for a motion"),
initial=4,
min_value=0,
max_value=8,
initial=4, min_value=0, max_value=8,
help_text=_("Choose 0 to disable the supporting system"),
)
motion_preamble = forms.CharField(

View File

@ -6,7 +6,10 @@
Models for the motion app.
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS.
To use a motion object, you only have to import the Motion class. Any
functionality can be reached from a motion object.
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
@ -34,39 +37,38 @@ from .workflow import (motion_workflow_choices, get_state, State, WorkflowError,
DUMMY_STATE)
# TODO: Save submitter and supporter in the same table
class MotionSubmitter(models.Model):
person = PersonField()
motion = models.ForeignKey('Motion', related_name="submitter")
def __unicode__(self):
return unicode(self.person)
class MotionSupporter(models.Model):
person = PersonField()
motion = models.ForeignKey('Motion', related_name="supporter")
def __unicode__(self):
return unicode(self.person)
class Motion(SlideMixin, models.Model):
"""
The Motion-Model.
"""
prefix = "motion"
"""The Motion Class.
This class is the main entry point to all other classes related to a motion.
"""
prefix = "motion"
"""Prefix for the slide system."""
active_version = models.ForeignKey('MotionVersion', null=True,
related_name="active_version")
"""Points to a specific version.
Used be the permitted-version-system to deside witch version is the active
Version. Could also be used to only choose a specific version as a default
version. Like the Sighted versions on Wikipedia.
"""
active_version = models.ForeignKey(
'MotionVersion', null=True, related_name="active_version")
state_id = models.CharField(max_length=3)
# Log (Translatable)
"""The id of a state object.
This Attribute is used be motion.state to identify the current state of the
motion.
"""
identifier = models.CharField(max_length=255, null=True, blank=True,
unique=True)
category = models.ForeignKey('Category', null=True, blank=True)
# TODO proposal
# Maybe rename to master_copy
master = models.ForeignKey('self', null=True, blank=True)
"""A string as human readable identifier for the motion."""
# category = models.ForeignKey('Category', null=True, blank=True)
# TODO: proposal
#master = models.ForeignKey('self', null=True, blank=True)
class Meta:
permissions = (
@ -79,17 +81,35 @@ class Motion(SlideMixin, models.Model):
# ordering = ('number',)
def __unicode__(self):
"""Return a human readable name of this motion."""
return self.get_title()
# TODO: Use transaction
def save(self, *args, **kwargs):
"""
Saves the motion. Create or update a motion_version object
"""Save the motion.
1. Set the state of a new motion to the default motion.
2. Save the motion object.
3. Save the version Data.
4. Set the active version for the motion.
A new version will be saved if motion.new_version was called
between the creation of this object and the last call of motion.save()
or
If the motion has new version data (title, text, reason)
and
the config 'motion_create_new_version' is set to
'ALLWASY_CREATE_NEW_VERSION'.
"""
if not self.state_id:
self.reset_state()
super(Motion, self).save(*args, **kwargs)
# Find out if the version data has changed
for attr in ['title', 'text', 'reason']:
if not self.versions.exists():
@ -109,7 +129,7 @@ class Motion(SlideMixin, models.Model):
elif new_data and not need_new_version:
version = self.last_version
else:
# We do not need to save the motion version
# We do not need to save the motion version.
return
# Save title, text and reason in the version object
@ -137,6 +157,10 @@ class Motion(SlideMixin, models.Model):
self.save()
def get_absolute_url(self, link='detail'):
"""Return an URL for this version.
The keywordargument 'link' can be 'detail', 'view', 'edit' or 'delete'.
"""
if link == 'view' or link == 'detail':
return reverse('motion_detail', args=[str(self.id)])
if link == 'edit':
@ -145,8 +169,9 @@ class Motion(SlideMixin, models.Model):
return reverse('motion_delete', args=[str(self.id)])
def get_title(self):
"""
Get the title of the motion. The titel is taken from motion.version
"""Get the title of the motion.
The titel is taken from motion.version.
"""
try:
return self._title
@ -154,16 +179,23 @@ class Motion(SlideMixin, models.Model):
return self.version.title
def set_title(self, title):
"""
Set the titel of the motion. The titel will me saved in motion.save()
"""Set the titel of the motion.
The titel will me saved into the version object, wenn motion.save() is
called.
"""
self._title = title
title = property(get_title, set_title)
"""The title of the motion.
Is saved in a MotionVersion object.
"""
def get_text(self):
"""
Get the text of the motion. Simular to get_title()
"""Get the text of the motion.
Simular to get_title().
"""
try:
return self._text
@ -171,16 +203,22 @@ class Motion(SlideMixin, models.Model):
return self.version.text
def set_text(self, text):
"""
Set the text of the motion. Simular to set_title()
""" Set the text of the motion.
Simular to set_title().
"""
self._text = text
text = property(get_text, set_text)
"""The text of a motin.
Is saved in a MotionVersion object.
"""
def get_reason(self):
"""
Get the reason of the motion. Simular to get_title()
"""Get the reason of the motion.
Simular to get_title().
"""
try:
return self._reason
@ -188,20 +226,26 @@ class Motion(SlideMixin, models.Model):
return self.version.reason
def set_reason(self, reason):
"""
Set the reason of the motion. Simular to set_title()
"""Set the reason of the motion.
Simular to set_title().
"""
self._reason = reason
reason = property(get_reason, set_reason)
"""The reason for the motion.
Is saved in a MotionVersion object.
"""
@property
def new_version(self):
"""
"""Return a Version object, not saved in the database.
On the first call, it creates a new version. On any later call, it
use the existing new version.
The new_version object will be deleted when it is saved into the db
The new_version object will be deleted when it is saved into the db.
"""
try:
return self._new_version
@ -210,9 +254,9 @@ class Motion(SlideMixin, models.Model):
return self._new_version
def get_version(self):
"""
Get the "active" version object. This version will be used to get the
data for this motion.
"""Get the 'active' version object.
This version will be used to get the data for this motion.
"""
try:
return self._version
@ -220,12 +264,12 @@ class Motion(SlideMixin, models.Model):
return self.last_version
def set_version(self, version):
"""
Set the "active" version object.
"""Set the 'active' version object.
If version is None, the last_version will be used.
If version is a version object, this object will be used.
If version is Int, the N version of this motion will be used.
The keyargument 'version' can be a MotionVersion object or the
version_number of a VersionObject or None.
If the argument is None, the newest version will be used.
"""
if version is None:
try:
@ -242,12 +286,11 @@ class Motion(SlideMixin, models.Model):
self._version = version
version = property(get_version, set_version)
"""The active version of this motion."""
@property
def last_version(self):
"""
Get the newest version of the motion
"""
"""Return the newest version of the motion."""
# TODO: Fix the case, that the motion has no Version
try:
return self.versions.order_by('-version_number')[0]
@ -255,15 +298,15 @@ class Motion(SlideMixin, models.Model):
return self.new_version
def is_submitter(self, person):
"""Return True, if person is a submitter of this motion. Else: False."""
self.submitter.filter(person=person).exists()
def is_supporter(self, person):
"""Return True, if person is a supporter of this motion. Else: False."""
return self.supporter.filter(person=person).exists()
def support(self, person):
"""
Add a Supporter to the list of supporters of the motion.
"""
"""Add 'person' as a supporter of this motion."""
if self.state.support:
if not self.is_supporter(person):
MotionSupporter(motion=self, person=person).save()
@ -271,17 +314,16 @@ class Motion(SlideMixin, models.Model):
raise WorkflowError("You can not support a motion in state %s" % self.state.name)
def unsupport(self, person):
"""
Remove a supporter from the list of supporters of the motion
"""
"""Remove 'person' as supporter from this motion."""
if self.state.support:
self.supporter.filter(person=person).delete()
else:
raise WorkflowError("You can not unsupport a motion in state %s" % self.state.name)
def create_poll(self):
"""
Create a new poll for this motion
"""Create a new poll for this motion.
Return the new poll object.
"""
if self.state.create_poll:
# TODO: auto increment the poll_number in the Database
@ -293,8 +335,9 @@ class Motion(SlideMixin, models.Model):
raise WorkflowError("You can not create a poll in state %s" % self.state.name)
def get_state(self):
"""
Get the state of this motion. Return a State object.
"""Return the state of the motion.
State is a State object. See openslides.motion.workflow for more informations.
"""
try:
return get_state(self.state_id)
@ -302,10 +345,10 @@ class Motion(SlideMixin, models.Model):
return DUMMY_STATE
def set_state(self, next_state):
"""
Set the state of this motion.
"""Set the state of this motion.
next_state has to be a valid state id or State object.
The keyargument 'next_state' has to be a State object or an id of a
State object.
"""
if not isinstance(next_state, State):
next_state = get_state(next_state)
@ -315,17 +358,14 @@ class Motion(SlideMixin, models.Model):
raise WorkflowError('%s is not a valid next_state' % next_state)
state = property(get_state, set_state)
"""The state of the motion as Ste object."""
def reset_state(self):
"""
Set the state to the default state.
"""
"""Set the state to the default state."""
self.state_id = get_state('default').id
def slide(self):
"""
return the slide dict
"""
"""Return the slide dict."""
data = super(Motion, self).slide()
data['motion'] = self
data['title'] = self.title
@ -333,6 +373,7 @@ class Motion(SlideMixin, models.Model):
return data
def get_agenda_title(self):
"""Return a title for the Agenda."""
return self.last_version.title
## def get_agenda_title_supplement(self):
@ -340,8 +381,7 @@ class Motion(SlideMixin, models.Model):
## return '(%s %s)' % (ugettext('motion'), number)
def get_allowed_actions(self, person):
"""
Gets a dictonary with all allowed actions for a specific person.
"""Return a dictonary with all allowed actions for a specific person.
The dictonary contains the following actions.
@ -374,11 +414,16 @@ class Motion(SlideMixin, models.Model):
return actions
def write_log(self, message, person=None):
"""Write a log message.
Message should be in english and translatable.
e.G: motion.write_log(ugettext_noob('Message Text'))
"""
MotionLog.objects.create(motion=self, message=message, person=person)
def activate_version(self, version):
"""
Activate a version of this motion.
"""Set the active state of a version to True.
'version' can be a version object, or the version_number of a version.
"""
@ -391,8 +436,7 @@ class Motion(SlideMixin, models.Model):
version.save()
def reject_version(self, version):
"""
Reject a version of this motion.
"""Reject a version of this motion.
'version' can be a version object, or the version_number of a version.
"""
@ -407,58 +451,126 @@ class Motion(SlideMixin, models.Model):
class MotionVersion(models.Model):
version_number = models.PositiveIntegerField(default=1)
title = models.CharField(max_length=255, verbose_name=ugettext_lazy("Title"))
text = models.TextField(verbose_name=_("Text"))
reason = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Reason"))
rejected = models.BooleanField(default=False)
creation_time = models.DateTimeField(auto_now=True)
"""
A MotionVersion object saves some date of the motion."""
motion = models.ForeignKey(Motion, related_name='versions')
identifier = models.CharField(max_length=255, verbose_name=ugettext_lazy("Version identifier"))
note = models.TextField(null=True, blank=True)
"""The Motion, to witch the version belongs."""
version_number = models.PositiveIntegerField(default=1)
"""An id for this version in realation to a motion.
Is unique for each motion.
"""
title = models.CharField(max_length=255, verbose_name=ugettext_lazy("Title"))
"""The Title of a motion."""
text = models.TextField(verbose_name=_("Text"))
"""The text of a motion."""
reason = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Reason"))
"""The reason for a motion."""
rejected = models.BooleanField(default=False)
"""Saves, if the version is rejected."""
creation_time = models.DateTimeField(auto_now=True)
"""Time, when the version was saved."""
#identifier = models.CharField(max_length=255, verbose_name=ugettext_lazy("Version identifier"))
#note = models.TextField(null=True, blank=True)
class Meta:
unique_together = ("motion", "version_number")
def __unicode__(self):
"""Return a string, representing this object."""
counter = self.version_number or _('new')
return "%s Version %s" % (self.motion, counter)
def get_absolute_url(self, link='detail'):
"""Return the URL of this Version.
The keyargument link can be 'view' or 'detail'.
"""
if link == 'view' or link == 'detail':
return reverse('motion_version_detail', args=[str(self.motion.id),
str(self.version_number)])
@property
def active(self):
"""Return True, if the version is the active version of a motion. Else: False."""
return self.active_version.exists()
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 MotionSubmitter(models.Model):
"""Save the submitter of a Motion."""
motion = models.ForeignKey('Motion', related_name="submitter")
"""The motion to witch the object belongs."""
person = PersonField()
"""The person, who is the submitter."""
def __unicode__(self):
return self.name
"""Return the name of the submitter as string."""
return unicode(self.person)
class Comment(models.Model):
motion_version = models.ForeignKey(MotionVersion)
text = models.TextField()
author = PersonField()
creation_time = models.DateTimeField(auto_now=True)
class MotionSupporter(models.Model):
"""Save the submitter of a Motion."""
motion = models.ForeignKey('Motion', related_name="supporter")
"""The motion to witch the object belongs."""
person = PersonField()
"""The person, who is the supporter."""
def __unicode__(self):
"""Return the name of the supporter as string."""
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"))
## def __unicode__(self):
## return self.name
## class Comment(models.Model):
## motion_version = models.ForeignKey(MotionVersion)
## text = models.TextField()
## author = PersonField()
## creation_time = models.DateTimeField(auto_now=True)
class MotionLog(models.Model):
"""Save a logmessage for a motion."""
motion = models.ForeignKey(Motion, related_name='log_messages')
"""The motion to witch the object belongs."""
message = models.CharField(max_length=255)
"""The log message.
Should be in english.
"""
person = PersonField(null=True)
"""A person object, who created the log message. Optional."""
time = models.DateTimeField(auto_now=True)
"""The Time, when the loged action was performed."""
class Meta:
ordering = ['-time']
def __unicode__(self):
"""Return a string, representing the log message."""
# TODO: write time in the local time format.
if self.person is None:
return "%s %s" % (self.time, _(self.message))
@ -467,33 +579,63 @@ class MotionLog(models.Model):
class MotionError(Exception):
"""Exception raised when errors in the motion accure."""
pass
class MotionVote(BaseVote):
"""Saves the votes for a MotionPoll.
There should allways be three MotionVote objects for each poll,
one for 'yes', 'no', and 'abstain'."""
option = models.ForeignKey('MotionOption')
"""The option object, to witch the vote belongs."""
class MotionOption(BaseOption):
"""Links between the MotionPollClass and the MotionVoteClass.
There should be one MotionOption object for each poll."""
poll = models.ForeignKey('MotionPoll')
"""The poll object, to witch the object belongs."""
vote_class = MotionVote
"""The VoteClass, to witch this Class links."""
class MotionPoll(CountInvalid, CountVotesCast, BasePoll):
option_class = MotionOption
vote_values = [
ugettext_noop('Yes'), ugettext_noop('No'), ugettext_noop('Abstain')]
"""The Class to saves the poll results for a motion poll."""
motion = models.ForeignKey(Motion, related_name='polls')
"""The motion to witch the object belongs."""
option_class = MotionOption
"""The option class, witch links between this object the the votes."""
vote_values = [
ugettext_noop('Yes'), ugettext_noop('No'), ugettext_noop('Abstain')]
"""The possible anwers for the poll. 'Yes, 'No' and 'Abstain'."""
poll_number = models.PositiveIntegerField(default=1)
"""An id for this poll in realation to a motion.
Is unique for each motion.
"""
class Meta:
unique_together = ("motion", "poll_number")
def __unicode__(self):
"""Return a string, representing the poll."""
return _('Ballot %d') % self.poll_number
def get_absolute_url(self, link='edit'):
"""Return an URL for the poll.
The keyargument 'link' can be 'edit' or 'delete'.
"""
if link == 'edit':
return reverse('motion_poll_edit', args=[str(self.motion.pk),
str(self.poll_number)])
@ -501,16 +643,13 @@ class MotionPoll(CountInvalid, CountVotesCast, BasePoll):
return reverse('motion_poll_delete', args=[str(self.motion.pk),
str(self.poll_number)])
def get_motion(self):
return self.motion
def set_options(self):
"""Create the option class for this poll."""
#TODO: maybe it is possible with .create() to call this without poll=self
# or call this in save()
self.get_option_class()(poll=self).save()
def append_pollform_fields(self, fields):
"""Apend the fields for invalid and votecast to the ModelForm."""
CountInvalid.append_pollform_fields(self, fields)
CountVotesCast.append_pollform_fields(self, fields)
def get_ballot(self):
return self.motion.motionpoll_set.filter(id__lte=self.id).count()

View File

@ -1,3 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
openslides.motion.pdf
~~~~~~~~~~~~~~~~~~~~~
Functions to generate the PDFs for the motion app.
"""
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.platypus import (
@ -11,6 +20,8 @@ from .models import Motion
def motions_to_pdf(pdf):
"""Create a PDF with all motions."""
motions = Motion.objects.all()
all_motion_cover(pdf, motions)
for motion in motions:
@ -19,6 +30,8 @@ def motions_to_pdf(pdf):
def motion_to_pdf(pdf, motion):
"""Create a PDF for one motion."""
pdf.append(Paragraph(_("Motion: %s") % motion.title, stylesheet['Heading1']))
motion_data = []
@ -141,6 +154,7 @@ def motion_to_pdf(pdf, motion):
def all_motion_cover(pdf, motions):
"""Create a coverpage for all motions."""
pdf.append(Paragraph(config["motion_pdf_title"], stylesheet['Heading1']))
preamble = config["motion_pdf_preamble"]

View File

@ -6,7 +6,7 @@
Signals for the motion app.
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS.
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
@ -18,6 +18,7 @@ from openslides.config.signals import default_config_value
@receiver(default_config_value, dispatch_uid="motion_default_config")
def default_config(sender, key, **kwargs):
"""Return the default config values for the motion app."""
return {
'motion_min_supporters': 0,
'motion_preamble': _('The assembly may decide,'),

View File

@ -1,3 +1,15 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
openslides.motion.slides
~~~~~~~~~~~~~~~~~~~~~~~~
Defines the Slides for the motion app.
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
from openslides.projector.api import register_slidemodel
from .models import Motion

View File

@ -2,11 +2,11 @@
# -*- coding: utf-8 -*-
"""
openslides.motion.urls
~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~
URL list for the motion app.
Defines the URL patterns for the motion app.
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS.
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""

View File

@ -6,10 +6,11 @@
Views for the motion app.
:copyright: 2011, 2012 by the OpenSlides team, see AUTHORS.
Will automaticly imported from openslides.motion.urls.py
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
from reportlab.platypus import Paragraph
from django.core.urlresolvers import reverse
from django.contrib import messages
@ -39,9 +40,7 @@ from .pdf import motions_to_pdf, motion_to_pdf
class MotionListView(ListView):
"""
List all motion.
"""
"""View, to list all motions."""
permission_required = 'motion.can_see_motion'
model = Motion
@ -49,7 +48,11 @@ motion_list = MotionListView.as_view()
class GetVersionMixin(object):
"""Mixin to set a specific version to a motion."""
def get_object(self):
"""Return a Motion object. The id is taken from the url and the version
is set to the version with the 'version_number' from the URL."""
object = super(GetVersionMixin, self).get_object()
version_number = self.kwargs.get('version_number', None)
if version_number is not None:
@ -61,14 +64,15 @@ class GetVersionMixin(object):
class MotionDetailView(GetVersionMixin, DetailView):
"""
Show the details of one motion.
"""
"""Show one motion."""
permission_required = 'motion.can_see_motion'
model = Motion
template_name = 'motion/motion_detail.html'
def get_context_data(self, **kwargs):
"""Return the template context.
Append the allowed actions for the motion to the context.
"""
context = super(MotionDetailView, self).get_context_data(**kwargs)
context['allowed_actions'] = self.object.get_allowed_actions(self.request.user)
return context
@ -77,10 +81,12 @@ motion_detail = MotionDetailView.as_view()
class MotionMixin(object):
"""
Mixin to add save the version-data to the motion-object
"""
"""Mixin for MotionViewsClasses, to save the version data."""
def manipulate_object(self, form):
"""Save the version data into the motion object before it is saved in
the Database."""
super(MotionMixin, self).manipulate_object(form)
for attr in ['title', 'text', 'reason']:
setattr(self.object, attr, form.cleaned_data[attr])
@ -92,6 +98,7 @@ class MotionMixin(object):
pass
def post_save(self, form):
"""Save the submitter an the supporter so the motion."""
super(MotionMixin, self).post_save(form)
# TODO: only delete and save neccessary submitters and supporter
if 'submitter' in form.cleaned_data:
@ -106,6 +113,13 @@ class MotionMixin(object):
for person in form.cleaned_data['supporter']])
def get_form_class(self):
"""Return the FormClass to Create or Update the Motion.
forms.BaseMotionForm is the base for the Class, and some FormMixins
will be mixed in dependence of some config values. See motion.forms
for more information on the mixins.
"""
form_classes = [BaseMotionForm]
if self.request.user.has_perm('motion.can_manage_motion'):
form_classes.append(MotionSubmitterMixin)
@ -117,13 +131,12 @@ class MotionMixin(object):
class MotionCreateView(MotionMixin, CreateView):
"""
Create a motion.
"""
"""View to create a motion."""
permission_required = 'motion.can_create_motion'
model = Motion
def form_valid(self, form):
"""Write a log message, if the form is valid."""
value = super(MotionCreateView, self).form_valid(form)
self.object.write_log(ugettext_noop('Motion created'), self.request.user)
return value
@ -132,15 +145,15 @@ motion_create = MotionCreateView.as_view()
class MotionUpdateView(MotionMixin, UpdateView):
"""
Update a motion.
"""
"""View to update a motion."""
model = Motion
def has_permission(self, request, *args, **kwargs):
"""Check, if the request.user has the permission to edit the motion."""
return self.get_object().get_allowed_actions(request.user)['edit']
def form_valid(self, form):
"""Write a log message, if the form is valid."""
value = super(MotionUpdateView, self).form_valid(form)
self.object.write_log(ugettext_noop('Motion updated'), self.request.user)
return value
@ -149,34 +162,39 @@ motion_edit = MotionUpdateView.as_view()
class MotionDeleteView(DeleteView):
"""
Delete one Motion.
"""
"""View to delete a motion."""
model = Motion
success_url_name = 'motion_list'
def has_permission(self, request, *args, **kwargs):
"""Check if the request.user has the permission to delete the motion."""
return self.get_object().get_allowed_actions(request.user)['delete']
motion_delete = MotionDeleteView.as_view()
class VersionPermitView(GetVersionMixin, SingleObjectMixin, QuestionMixin, RedirectView):
"""View to permit a version of a motion."""
model = Motion
question_url_name = 'motion_version_detail'
success_url_name = 'motion_version_detail'
def get(self, *args, **kwargs):
"""Set self.object to a motion."""
self.object = self.get_object()
return super(VersionPermitView, self).get(*args, **kwargs)
def get_url_name_args(self):
"""Return a list with arguments to create the success- and question_url."""
return [self.object.pk, self.object.version.version_number]
def get_question(self):
"""Return a string, shown to the user as question to permit the version."""
return _('Are you sure you want permit Version %s?') % self.object.version.version_number
def case_yes(self):
"""Activate the version, if the user chooses 'yes'."""
self.object.activate_version(self.object.version)
self.object.save()
@ -184,21 +202,25 @@ version_permit = VersionPermitView.as_view()
class VersionRejectView(GetVersionMixin, SingleObjectMixin, QuestionMixin, RedirectView):
"""View to reject a version."""
model = Motion
question_url_name = 'motion_version_detail'
success_url_name = 'motion_version_detail'
def get(self, *args, **kwargs):
"""Set self.object to a motion."""
self.object = self.get_object()
return super(VersionRejectView, self).get(*args, **kwargs)
def get_url_name_args(self):
"""Return a list with arguments to create the success- and question_url."""
return [self.object.pk, self.object.version.version_number]
def get_question(self):
return _('Are you sure you want reject Version %s?') % self.object.version.version_number
def case_yes(self):
"""Reject the version, if the user chooses 'yes'."""
self.object.reject_version(self.object.version)
self.object.save()
@ -206,23 +228,25 @@ version_reject = VersionRejectView.as_view()
class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
"""View to support or unsupport a motion.
If self.support is True, the view will append a request.user to the supporter list.
If self.support is False, the view will remove a request.user from the supporter list.
"""
Classed based view to support or unsupport a motion. Use
support=True or support=False in urls.py
"""
permission_required = 'motion.can_support_motion'
model = Motion
support = True
def get(self, request, *args, **kwargs):
"""Set self.object to a motion."""
self.object = self.get_object()
return super(SupportView, self).get(request, *args, **kwargs)
def check_permission(self, request):
"""
Checks whether request.user can support or unsupport the motion.
Returns True or False.
"""
"""Return True if the user can support or unsupport the motion. Else: False."""
allowed_actions = self.object.get_allowed_actions(request.user)
if self.support and not allowed_actions['support']:
messages.error(request, _('You can not support this motion.'))
@ -233,17 +257,19 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
else:
return True
def pre_redirect(self, request, *args, **kwargs):
if self.check_permission(request):
super(SupportView, self).pre_redirect(request, *args, **kwargs)
def get_question(self):
"""Return the question string."""
if self.support:
return _('Do you really want to support this motion?')
else:
return _('Do you really want to unsupport this motion?')
def case_yes(self):
"""Append or remove the request.user from the motion.
First the methode checks the permissions, and writes a log message after
appending or removing the user.
"""
if self.check_permission(self.request):
user = self.request.user
if self.support:
@ -254,12 +280,14 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
self.object.write_log(ugettext_noop("Supporter: -%s") % user, user)
def get_success_message(self):
"""Return the success message."""
if self.support:
return _("You have supported this motion successfully.")
else:
return _("You have unsupported this motion successfully.")
def get_redirect_url(self, **kwargs):
"""Return the url, the view should redirect to."""
return self.object.get_absolute_url()
motion_support = SupportView.as_view(support=True)
@ -267,48 +295,68 @@ motion_unsupport = SupportView.as_view(support=False)
class PollCreateView(SingleObjectMixin, RedirectView):
"""View to create a poll for a motion."""
permission_required = 'motion.can_manage_motion'
model = Motion
def get(self, request, *args, **kwargs):
"""Set self.object to a motion."""
self.object = self.get_object()
return super(PollCreateView, self).get(request, *args, **kwargs)
def pre_redirect(self, request, *args, **kwargs):
"""Create the poll for the motion."""
self.poll = self.object.create_poll()
self.object.write_log(ugettext_noop("Poll created"), request.user)
messages.success(request, _("New vote was successfully created."))
def get_redirect_url(self, **kwargs):
"""Return the URL to the EditView of the poll."""
return reverse('motion_poll_edit', args=[self.object.pk, self.poll.poll_number])
poll_create = PollCreateView.as_view()
class PollMixin(object):
"""Mixin for the PollUpdateView and the PollDeleteView."""
permission_required = 'motion.can_manage_motion'
success_url_name = 'motion_detail'
def get_object(self):
"""Return a MotionPoll object.
Use the motion id and the poll_number from the url kwargs to get the
object.
"""
return MotionPoll.objects.filter(
motion=self.kwargs['pk'],
poll_number=self.kwargs['poll_number']).get()
def get_url_name_args(self):
"""Return the arguments to create the url to the success_url"""
return [self.object.motion.pk]
class PollUpdateView(PollMixin, PollFormView):
"""View to update a MotionPoll."""
poll_class = MotionPoll
"""Poll Class to use for this view."""
template_name = 'motion/poll_form.html'
def get_context_data(self, **kwargs):
"""Return the template context.
Append the motion object to the context.
"""
context = super(PollUpdateView, self).get_context_data(**kwargs)
context.update({
'motion': self.poll.motion})
return context
def form_valid(self, form):
"""Write a log message, if the form is valid."""
value = super(PollUpdateView, self).form_valid(form)
self.object.write_log(ugettext_noop('Poll updated'), self.request.user)
return value
@ -317,9 +365,11 @@ poll_edit = PollUpdateView.as_view()
class PollDeleteView(PollMixin, DeleteView):
"""View to delete a MotionPoll."""
model = MotionPoll
def case_yes(self):
"""Write a log message, if the form is valid."""
super(PollDeleteView, self).case_yes()
self.object.write_log(ugettext_noop('Poll deleted'), self.request.user)
@ -327,12 +377,19 @@ poll_delete = PollDeleteView.as_view()
class MotionSetStateView(SingleObjectMixin, RedirectView):
"""View to set the state of a motion.
If self.reset is False, the new state is taken from url.
If self.reset is True, the default state is taken.
"""
permission_required = 'motion.can_manage_motion'
url_name = 'motion_detail'
model = Motion
reset = False
def pre_redirect(self, request, *args, **kwargs):
"""Save the new state and write a log message."""
self.object = self.get_object()
try:
if self.reset:
@ -350,6 +407,7 @@ class MotionSetStateView(SingleObjectMixin, RedirectView):
% html_strong(self.object.state)))
def get_url_name_args(self):
"""Return the arguments to generate the redirect_url."""
return [self.object.pk]
set_state = MotionSetStateView.as_view()
@ -357,15 +415,18 @@ reset_state = MotionSetStateView.as_view(reset=True)
class CreateAgendaItemView(SingleObjectMixin, RedirectView):
"""View to create and agenda item for a motion."""
permission_required = 'agenda.can_manage_agenda'
url_name = 'item_overview'
model = Motion
def get(self, request, *args, **kwargs):
"""Set self.object to a motion."""
self.object = self.get_object()
return super(CreateAgendaItemView, self).get(request, *args, **kwargs)
def pre_redirect(self, request, *args, **kwargs):
"""Create the agenda item."""
self.item = Item.objects.create(related_sid=self.object.sid)
self.object.write_log(ugettext_noop('Created Agenda Item'), self.request.user)
@ -373,23 +434,32 @@ create_agenda_item = CreateAgendaItemView.as_view()
class MotionPDFView(SingleObjectMixin, PDFView):
"""Create the PDF for one, or all motions.
If self.print_all_motions is True, the view returns a PDF with all motions.
If self.print_all_motions is False, the view returns a PDF with only one
motion."""
permission_required = 'motion.can_manage_motion'
model = Motion
top_space = 0
print_all_motions = False
def get(self, request, *args, **kwargs):
"""Set self.object to a motion."""
if not self.print_all_motions:
self.object = self.get_object()
return super(MotionPDFView, self).get(request, *args, **kwargs)
def get_filename(self):
"""Return the filename for the PDF."""
if self.print_all_motions:
return _("Motions")
else:
return _("Motion: %s") % unicode(self.object)
def append_to_pdf(self, pdf):
"""Append PDF objects."""
if self.print_all_motions:
motions_to_pdf(pdf)
else:
@ -400,6 +470,7 @@ motion_detail_pdf = MotionPDFView.as_view(print_all_motions=False)
class Config(FormView):
"""The View for the config tab."""
permission_required = 'config.can_manage_config'
form_class = ConfigForm
template_name = 'motion/config.html'
@ -431,9 +502,8 @@ class Config(FormView):
def register_tab(request):
"""
Register the projector tab.
"""
"""Return the motion tab."""
# TODO: Find a bether way to set the selected var.
selected = request.path.startswith('/motion/')
return Tab(
title=_('Motions'),
@ -445,6 +515,10 @@ def register_tab(request):
def get_widgets(request):
"""Return the motion widgets for the dashboard.
There is only one widget. It shows all motions.
"""
return [Widget(
name='motions',
display_name=_('Motions'),

View File

@ -1,16 +1,44 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
openslides.utils.workflow
~~~~~~~~~~~~~~~~~~~~~~~~~
Defines the States for motions. All States are linked together with there
'next_state' attributes. Together there are a workflow.
:copyright: (c) 2011-2013 by the OpenSlides team, see AUTHORS.
:license: GNU GPL, see LICENSE for more details.
"""
from django.conf import settings
from django.core import exceptions
from django.utils.importlib import import_module
from django.utils.translation import ugettext_noop,
from openslides.config.models import config
ugettext = lambda s: s
_workflow = None
class State(object):
"""Define a state for a motion."""
def __init__(self, id, name, next_states=[], create_poll=False, support=False,
edit_as_submitter=False, version_permission=True):
"""Set attributes for the state.
The Arguments are:
- 'id' a unique id for the state.
- 'name' a string representing the state.
- 'next_states' a list with all states, that can be choosen from this state.
All the other arguments are boolean values. If True, the specific action for
motions in this state.
- 'create_poll': polls can be created in this state.
- 'support': persons can support the motion in this state.
- 'edit_as_submitter': the submitter can edit the motion in this state.
- 'version_permission': new versions are not permitted.
"""
self.id = id
self.name = name
self.next_states = next_states
@ -20,19 +48,34 @@ class State(object):
self.version_permission = version_permission
def __unicode__(self):
"""Return the name of the state."""
return self.name
class WorkflowError(Exception):
"""Exception raised when errors in a state accure."""
pass
def motion_workflow_choices():
"""Return all possible workflows.
The possible workflows can be set in the settings with the setting
'MOTION_WORKFLOW'.
"""
for workflow in settings.MOTION_WORKFLOW:
yield workflow[0], workflow[1]
def get_state(state='default'):
"""Return a state object.
The argument 'state' has to be a state_id.
If the argument 'state' is 'default', the default state is returned.
The default state is the state object choosen in the config tab.
"""
global _workflow
if _workflow is not None:
try:
@ -68,24 +111,34 @@ def get_state(state='default'):
def populate_workflow(state, workflow):
"""Append all 'next_states' from state to the workflow.
The argument state has to be a state object.
The argument workflow has to be a dictonary.
Calls this function recrusiv with all next_states from the next_states states.
"""
workflow[state.id] = state
for s in state.next_states:
if s.id not in workflow:
populate_workflow(s, workflow)
DUMMY_STATE = State('dummy', ugettext('Unknwon state'))
DUMMY_STATE = State('dummy', ugettext_noop('Unknwon state'))
"""A dummy state object. Returned, if the state_id is not known."""
default_workflow = State('pub', ugettext('Published'), support=True,
default_workflow = State('pub', ugettext_noop('Published'), support=True,
edit_as_submitter=True, version_permission=False)
"""Default Workflow for OpenSlides."""
default_workflow.next_states = [
State('per', ugettext('Permitted'), create_poll=True, edit_as_submitter=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)'))]
State('per', ugettext_noop('Permitted'), create_poll=True, edit_as_submitter=True, next_states=[
State('acc', ugettext_noop('Accepted')),
State('rej', ugettext_noop('Rejected')),
State('wit', ugettext_noop('Withdrawed')),
State('adj', ugettext_noop('Adjourned')),
State('noc', ugettext_noop('Not Concerned')),
State('com', ugettext_noop('Commited a bill')),
State('rev', ugettext_noop('Needs Review'))]),
State('nop', ugettext_noop('Rejected (not authorized)'))]