diff --git a/openslides/motion/__init__.py b/openslides/motion/__init__.py index e1cf60db8..d9b4927bc 100644 --- a/openslides/motion/__init__.py +++ b/openslides/motion/__init__.py @@ -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 diff --git a/openslides/motion/forms.py b/openslides/motion/forms.py index b488a7a2a..ed1b02155 100644 --- a/openslides/motion/forms.py +++ b/openslides/motion/forms.py @@ -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( diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 47aa9ad72..cf39e3410 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -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() diff --git a/openslides/motion/pdf.py b/openslides/motion/pdf.py index d6e3cb743..fcee6c2e7 100644 --- a/openslides/motion/pdf.py +++ b/openslides/motion/pdf.py @@ -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"] diff --git a/openslides/motion/signals.py b/openslides/motion/signals.py index b482cf38c..fb1dffd9a 100644 --- a/openslides/motion/signals.py +++ b/openslides/motion/signals.py @@ -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,'), diff --git a/openslides/motion/slides.py b/openslides/motion/slides.py index 9f93fdd38..e269dc243 100644 --- a/openslides/motion/slides.py +++ b/openslides/motion/slides.py @@ -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 diff --git a/openslides/motion/urls.py b/openslides/motion/urls.py index 883b4c953..8ff7c0d33 100644 --- a/openslides/motion/urls.py +++ b/openslides/motion/urls.py @@ -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. """ diff --git a/openslides/motion/views.py b/openslides/motion/views.py index 2b7e89784..9f26faec1 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -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'), diff --git a/openslides/motion/workflow.py b/openslides/motion/workflow.py index 813e0c537..2efbf803e 100644 --- a/openslides/motion/workflow.py +++ b/openslides/motion/workflow.py @@ -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)'))]