OpenSlides/openslides/motion/models.py

866 lines
28 KiB
Python
Raw Normal View History

2011-07-31 10:46:29 +02:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
2012-10-24 11:04:23 +02:00
openslides.motion.models
~~~~~~~~~~~~~~~~~~~~~~~~
2011-07-31 10:46:29 +02:00
2012-10-24 11:04:23 +02:00
Models for the motion app.
2011-07-31 10:46:29 +02:00
2013-02-05 18:46:46 +01:00
To use a motion object, you only have to import the Motion class. Any
functionality can be reached from a motion object.
:copyright: (c) 20112013 by the OpenSlides team, see AUTHORS.
2011-07-31 10:46:29 +02:00
:license: GNU GPL, see LICENSE for more details.
"""
from datetime import datetime
2012-04-12 16:21:30 +02:00
from django.core.urlresolvers import reverse
2013-03-12 22:03:56 +01:00
from django.db import models, IntegrityError
from django.db.models import Max
from django.dispatch import receiver
from django.utils import formats
2012-06-30 15:21:27 +02:00
from django.utils.translation import pgettext
2013-01-26 16:33:55 +01:00
from django.utils.translation import ugettext_lazy, ugettext_noop, ugettext as _
2011-07-31 10:46:29 +02:00
from openslides.utils.person import PersonField
from openslides.config.api import config
from openslides.poll.models import (
BaseOption, BasePoll, CountVotesCast, CountInvalid, BaseVote)
from openslides.participant.models import User
from openslides.projector.api import register_slidemodel
from openslides.projector.models import SlideMixin
from openslides.agenda.models import Item
from .exceptions import MotionError, WorkflowError
2013-02-17 17:07:44 +01:00
2013-02-05 18:46:46 +01:00
class Motion(SlideMixin, models.Model):
2013-04-19 14:12:49 +02:00
"""
The Motion Class.
2013-01-26 12:28:51 +01:00
2013-02-05 18:46:46 +01:00
This class is the main entry point to all other classes related to a motion.
"""
2013-01-26 12:28:51 +01:00
prefix = 'motion'
2013-04-19 14:12:49 +02:00
"""
Prefix for the slide system.
"""
2011-07-31 10:46:29 +02:00
2013-02-05 18:46:46 +01:00
active_version = models.ForeignKey('MotionVersion', null=True,
related_name="active_version")
2013-04-19 14:12:49 +02:00
"""
Points to a specific version.
2013-01-26 15:25:54 +01:00
2013-03-14 22:54:00 +01:00
Used be the permitted-version-system to deside which 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.
2013-02-05 18:46:46 +01:00
"""
2013-01-06 12:07:37 +01:00
state = models.ForeignKey('State', null=True) # TODO: Check whether null=True is necessary.
2013-04-19 14:12:49 +02:00
"""
The related state object.
2013-01-06 12:07:37 +01:00
This attribute is to get the current state of the motion.
2013-01-26 12:28:51 +01:00
"""
2013-01-06 12:07:37 +01:00
identifier = models.CharField(max_length=255, null=True, blank=True,
unique=True)
2013-04-19 14:12:49 +02:00
"""
A string as human readable identifier for the motion.
"""
2013-02-05 18:46:46 +01:00
2013-03-12 22:03:56 +01:00
identifier_number = models.IntegerField(null=True)
2013-04-19 14:12:49 +02:00
"""
Counts the number of the motion in one category.
2013-03-12 22:03:56 +01:00
Needed to find the next free motion-identifier.
"""
2013-03-11 20:17:19 +01:00
category = models.ForeignKey('Category', null=True, blank=True)
2013-04-19 14:12:49 +02:00
"""
ForeignKey to one category of motions.
"""
2013-03-11 20:17:19 +01:00
2013-02-05 18:46:46 +01:00
# TODO: proposal
#master = models.ForeignKey('self', null=True, blank=True)
2011-07-31 10:46:29 +02:00
2013-01-06 12:07:37 +01:00
class Meta:
permissions = (
('can_see_motion', ugettext_noop('Can see motions')),
('can_create_motion', ugettext_noop('Can create motions')),
('can_support_motion', ugettext_noop('Can support motions')),
('can_manage_motion', ugettext_noop('Can manage motions')),
)
# TODO: order per default by category and identifier
# ordering = ('number',)
2013-01-06 12:07:37 +01:00
def __unicode__(self):
2013-04-19 14:12:49 +02:00
"""
Return a human readable name of this motion.
"""
2013-01-06 12:07:37 +01:00
return self.get_title()
# TODO: Use transaction
2013-04-19 16:02:16 +02:00
def save(self, no_new_version=False, *args, **kwargs):
2013-04-19 14:12:49 +02:00
"""
Save the motion.
2013-02-05 18:46:46 +01:00
1. Set the state of a new motion to the default state.
2013-02-05 18:46:46 +01:00
2. Save the motion object.
3. Save the version data.
2013-02-05 18:46:46 +01:00
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
'ALWAYS_CREATE_NEW_VERSION'.
2013-04-19 16:02:16 +02:00
If no_new_version is True, a new version will never be used.
2013-01-26 12:28:51 +01:00
"""
if not self.state:
self.reset_state()
2013-02-01 16:33:45 +01:00
2013-04-19 14:12:49 +02:00
if not self.identifier and self.identifier is not None:
self.identifier = None
2013-01-06 12:07:37 +01:00
super(Motion, self).save(*args, **kwargs)
2013-02-05 18:46:46 +01:00
2013-04-19 16:02:16 +02:00
if no_new_version:
return
2013-02-03 13:54:01 +01:00
# Find out if the version data has changed
for attr in ['title', 'text', 'reason']:
2013-02-03 13:54:01 +01:00
if not self.versions.exists():
new_data = True
break
if getattr(self, attr) != getattr(self.last_version, attr):
2013-01-06 12:07:37 +01:00
new_data = True
break
else:
new_data = False
2013-04-19 16:02:16 +02:00
# TODO: Check everything here. The decision whether to create a new
# version has to be done in the view. Update docstings too.
need_new_version = self.state.versioning
if hasattr(self, '_new_version') or (new_data and need_new_version):
2013-01-06 12:07:37 +01:00
version = self.new_version
del self._new_version
version.motion = self # TODO: Test if this line is really neccessary.
2013-01-06 12:07:37 +01:00
elif new_data and not need_new_version:
version = self.last_version
else:
2013-02-05 18:46:46 +01:00
# We do not need to save the motion version.
return
2013-02-03 13:24:29 +01:00
2013-02-03 13:54:01 +01:00
# Save title, text and reason in the version object
2013-01-06 12:07:37 +01:00
for attr in ['title', 'text', 'reason']:
_attr = '_%s' % attr
2011-07-31 10:46:29 +02:00
try:
2013-01-06 12:07:37 +01:00
setattr(version, attr, getattr(self, _attr))
delattr(self, _attr)
2011-07-31 10:46:29 +02:00
except AttributeError:
2013-02-03 13:54:01 +01:00
if self.versions.exists():
# If the _attr was not set, use the value from last_version
setattr(version, attr, getattr(self.last_version, attr))
2013-02-03 13:24:29 +01:00
2013-02-03 13:54:01 +01:00
# Set version_number of the new Version (if neccessary) and save it into the DB
2013-02-03 13:24:29 +01:00
if version.id is None:
# TODO: auto increment the version_number in the Database
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
version.version_number = version_number + 1
2013-01-06 12:07:37 +01:00
version.save()
2012-02-14 16:31:21 +01:00
# Set the active version of this motion. This has to be done after the
# version is saved to the database
if self.active_version is None or not self.state.leave_old_version_active:
2013-02-03 13:24:29 +01:00
self.active_version = version
self.save()
2013-01-06 12:07:37 +01:00
def get_absolute_url(self, link='detail'):
2013-04-19 14:12:49 +02:00
"""
Return an URL for this version.
2013-02-05 18:46:46 +01:00
The keyword argument 'link' can be 'detail', 'view', 'edit' or 'delete'.
2013-02-05 18:46:46 +01:00
"""
2013-01-06 12:07:37 +01:00
if link == 'view' or link == 'detail':
return reverse('motion_detail', args=[str(self.id)])
if link == 'edit':
2012-10-24 11:04:23 +02:00
return reverse('motion_edit', args=[str(self.id)])
2011-07-31 10:46:29 +02:00
if link == 'delete':
2012-10-24 11:04:23 +02:00
return reverse('motion_delete', args=[str(self.id)])
2011-07-31 10:46:29 +02:00
2013-03-12 22:03:56 +01:00
def set_identifier(self):
2013-03-12 23:35:08 +01:00
if config['motion_identifier'] == 'manually':
# Do not set an identifier.
return
elif config['motion_identifier'] == 'per_category':
motions = Motion.objects.filter(category=self.category)
2013-03-12 22:03:56 +01:00
else:
2013-03-12 23:35:08 +01:00
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:
2013-03-12 22:03:56 +01:00
prefix = ''
2013-03-12 23:35:08 +01:00
else:
prefix = self.category.prefix + ' '
2013-03-12 22:03:56 +01:00
while True:
number += 1
self.identifier = '%s%d' % (prefix, number)
try:
self.save()
except IntegrityError:
continue
else:
self.number = number
self.save()
break
2013-01-06 12:07:37 +01:00
def get_title(self):
2013-04-19 14:12:49 +02:00
"""
Get the title of the motion.
2013-02-05 18:46:46 +01:00
The titel is taken from motion.version.
2013-01-26 12:28:51 +01:00
"""
2011-07-31 10:46:29 +02:00
try:
2013-01-06 12:07:37 +01:00
return self._title
2011-07-31 10:46:29 +02:00
except AttributeError:
2013-01-26 12:28:51 +01:00
return self.version.title
2011-07-31 10:46:29 +02:00
2013-01-06 12:07:37 +01:00
def set_title(self, title):
2013-04-19 14:12:49 +02:00
"""
Set the titel of the motion.
2013-02-05 18:46:46 +01:00
The titel will me saved into the version object, wenn motion.save() is
called.
2013-01-26 12:28:51 +01:00
"""
2013-01-06 12:07:37 +01:00
self._title = title
2011-07-31 10:46:29 +02:00
2013-01-06 12:07:37 +01:00
title = property(get_title, set_title)
2013-04-19 14:12:49 +02:00
"""
The title of the motion.
2013-02-05 18:46:46 +01:00
Is saved in a MotionVersion object.
"""
2011-07-31 10:46:29 +02:00
2013-01-06 12:07:37 +01:00
def get_text(self):
2013-04-19 14:12:49 +02:00
"""
Get the text of the motion.
2013-02-05 18:46:46 +01:00
Simular to get_title().
2013-01-26 12:28:51 +01:00
"""
2013-01-06 12:07:37 +01:00
try:
return self._text
except AttributeError:
2013-01-26 12:28:51 +01:00
return self.version.text
2011-07-31 10:46:29 +02:00
2013-01-06 12:07:37 +01:00
def set_text(self, text):
2013-04-19 14:12:49 +02:00
"""
Set the text of the motion.
2013-02-05 18:46:46 +01:00
Simular to set_title().
2013-01-26 12:28:51 +01:00
"""
2013-01-06 12:07:37 +01:00
self._text = text
text = property(get_text, set_text)
2013-04-19 14:12:49 +02:00
"""
The text of a motin.
2013-02-05 18:46:46 +01:00
Is saved in a MotionVersion object.
"""
2013-01-06 12:07:37 +01:00
def get_reason(self):
2013-04-19 14:12:49 +02:00
"""
Get the reason of the motion.
2013-02-05 18:46:46 +01:00
Simular to get_title().
2013-01-26 12:28:51 +01:00
"""
2013-01-06 12:07:37 +01:00
try:
return self._reason
except AttributeError:
2013-01-26 12:28:51 +01:00
return self.version.reason
2013-01-06 12:07:37 +01:00
def set_reason(self, reason):
2013-04-19 14:12:49 +02:00
"""
Set the reason of the motion.
2013-02-05 18:46:46 +01:00
Simular to set_title().
2013-01-26 12:28:51 +01:00
"""
2013-01-06 12:07:37 +01:00
self._reason = reason
reason = property(get_reason, set_reason)
2013-04-19 14:12:49 +02:00
"""
The reason for the motion.
2013-02-05 18:46:46 +01:00
Is saved in a MotionVersion object.
"""
2011-07-31 10:46:29 +02:00
2013-01-26 12:28:51 +01:00
@property
def new_version(self):
2013-04-19 14:12:49 +02:00
"""
Return a version object, not saved in the database.
2013-02-05 18:46:46 +01:00
2013-01-26 12:28:51 +01:00
On the first call, it creates a new version. On any later call, it
use the existing new version.
2013-02-05 18:46:46 +01:00
The new_version object will be deleted when it is saved into the db.
2013-01-26 12:28:51 +01:00
"""
try:
return self._new_version
except AttributeError:
self._new_version = MotionVersion(motion=self)
return self._new_version
def get_version(self):
2013-04-19 14:12:49 +02:00
"""
Get the 'active' version object.
2013-02-05 18:46:46 +01:00
This version will be used to get the data for this motion.
2013-01-26 12:28:51 +01:00
"""
2013-01-06 12:07:37 +01:00
try:
2013-01-26 12:28:51 +01:00
return self._version
2013-01-06 12:07:37 +01:00
except AttributeError:
return self.last_version
2012-02-14 16:31:21 +01:00
2013-01-26 12:28:51 +01:00
def set_version(self, version):
2013-04-19 14:12:49 +02:00
"""
Set the 'active' version object.
2013-02-05 18:46:46 +01:00
The keyword argument 'version' can be a MotionVersion object or the
version_number of a version object or None.
2013-01-26 12:28:51 +01:00
2013-02-05 18:46:46 +01:00
If the argument is None, the newest version will be used.
2013-01-26 12:28:51 +01:00
"""
2013-01-06 12:07:37 +01:00
if version is None:
try:
2013-01-26 12:28:51 +01:00
del self._version
2013-01-06 12:07:37 +01:00
except AttributeError:
pass
else:
if type(version) is int:
2013-02-03 13:24:29 +01:00
version = self.versions.get(version_number=version)
2013-01-06 12:07:37 +01:00
elif type(version) is not MotionVersion:
raise ValueError('The argument \'version\' has to be int or '
'MotionVersion, not %s' % type(version))
2013-01-26 12:28:51 +01:00
# TODO: Test, that the version is one of this motion
self._version = version
2012-02-14 16:31:21 +01:00
2013-01-26 12:28:51 +01:00
version = property(get_version, set_version)
2013-04-19 14:12:49 +02:00
"""
The active version of this motion.
"""
2012-02-15 12:36:50 +01:00
2013-01-06 12:07:37 +01:00
@property
def last_version(self):
2013-04-19 14:12:49 +02:00
"""
Return the newest version of the motion.
"""
# TODO: Fix the case, that the motion has no version
2013-01-06 12:07:37 +01:00
try:
2013-02-03 13:24:29 +01:00
return self.versions.order_by('-version_number')[0]
2013-01-06 12:07:37 +01:00
except IndexError:
return self.new_version
@property
def submitters(self):
return sorted([object.person for object in self.submitter.all()],
key=lambda person: person.sort_name)
def is_submitter(self, person):
2013-02-05 18:46:46 +01:00
"""Return True, if person is a submitter of this motion. Else: False."""
2013-03-15 11:35:03 +01:00
return self.submitter.filter(person=person).exists()
@property
def supporters(self):
return sorted([object.person for object in self.supporter.all()],
key=lambda person: person.sort_name)
2013-03-15 11:35:03 +01:00
def add_submitter(self, person):
MotionSubmitter.objects.create(motion=self, person=person)
def is_supporter(self, person):
2013-04-19 14:12:49 +02:00
"""
Return True, if person is a supporter of this motion. Else: False.
"""
return self.supporter.filter(person=person).exists()
def support(self, person):
2013-04-19 14:12:49 +02:00
"""
Add 'person' as a supporter of this motion.
"""
if self.state.allow_support:
if not self.is_supporter(person):
MotionSupporter(motion=self, person=person).save()
else:
raise WorkflowError('You can not support a motion in state %s.' % self.state.name)
def unsupport(self, person):
2013-04-19 14:12:49 +02:00
"""
Remove 'person' as supporter from this motion.
"""
if self.state.allow_support:
self.supporter.filter(person=person).delete()
else:
raise WorkflowError('You can not unsupport a motion in state %s.' % self.state.name)
2013-02-01 12:51:54 +01:00
def create_poll(self):
2013-04-19 14:12:49 +02:00
"""
Create a new poll for this motion.
2013-02-05 18:46:46 +01:00
Return the new poll object.
2013-02-01 16:33:45 +01:00
"""
if self.state.allow_create_poll:
# TODO: auto increment the poll_number in the database
poll_number = self.polls.aggregate(Max('poll_number'))['poll_number__max'] or 0
poll = MotionPoll.objects.create(motion=self, poll_number=poll_number + 1)
poll.set_options()
return poll
else:
raise WorkflowError('You can not create a poll in state %s.' % self.state.name)
2013-02-01 16:33:45 +01:00
2013-03-12 22:03:56 +01:00
def set_state(self, state):
2013-04-19 14:12:49 +02:00
"""
Set the state of the motion.
2013-03-12 22:03:56 +01:00
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:
2013-03-12 22:03:56 +01:00
self.set_identifier()
self.state = state
def reset_state(self):
2013-04-19 14:12:49 +02:00
"""
Set the state to the default state.
If the motion is new, it chooses the default workflow from config.
"""
if self.state:
self.state = self.state.workflow.first_state
else:
self.state = (Workflow.objects.get(pk=config['motion_workflow']).first_state or
Workflow.objects.get(pk=config['motion_workflow']).state_set.all()[0])
2013-02-02 00:37:43 +01:00
def slide(self):
2013-04-19 14:12:49 +02:00
"""
Return the slide dict.
"""
2013-02-02 00:37:43 +01:00
data = super(Motion, self).slide()
data['motion'] = self
data['title'] = self.title
data['template'] = 'projector/Motion.html'
return data
2013-02-02 00:51:08 +01:00
def get_agenda_title(self):
2013-04-19 14:12:49 +02:00
"""
Return a title for the Agenda.
"""
return self.last_version.title # TODO: nutze active_version
2013-02-02 00:51:08 +01:00
## def get_agenda_title_supplement(self):
## number = self.number or '<i>[%s]</i>' % ugettext('no number')
## return '(%s %s)' % (ugettext('motion'), number)
def get_allowed_actions(self, person):
2013-04-19 14:12:49 +02:00
"""
Return a dictonary with all allowed actions for a specific person.
The dictonary contains the following actions.
* edit
* delete
* create_poll
* support
* unsupport
* change_state
* reset_state
"""
actions = {
2013-02-02 10:52:13 +01:00
'edit': ((self.is_submitter(person) and
self.state.allow_submitter_edit) or
2013-02-02 10:52:13 +01:00
person.has_perm('motion.can_manage_motion')),
2013-02-02 10:52:13 +01:00
'create_poll': (person.has_perm('motion.can_manage_motion') and
self.state.allow_create_poll),
'support': (self.state.allow_support and
2013-02-02 10:52:13 +01:00
config['motion_min_supporters'] > 0 and
not self.is_submitter(person) and
not self.is_supporter(person)),
'unsupport': (self.state.allow_support and
not self.is_submitter(person) and
self.is_supporter(person)),
'change_state': person.has_perm('motion.can_manage_motion'),
}
2013-02-02 10:52:13 +01:00
actions['delete'] = actions['edit'] # TODO: Only if the motion has no number
actions['reset_state'] = actions['change_state']
return actions
def write_log(self, message, person=None):
2013-02-05 18:46:46 +01:00
"""Write a log message.
Message should be in english and translatable.
e.g.: motion.write_log(ugettext_noob('Message Text'))
2013-02-05 18:46:46 +01:00
"""
MotionLog.objects.create(motion=self, message=message, person=person)
2013-04-19 16:02:16 +02:00
def set_active_version(self, version):
2013-04-19 14:12:49 +02:00
"""
2013-04-19 16:02:16 +02:00
Set the active state of a version to 'version'.
2013-02-03 13:24:29 +01:00
'version' can be a version object, or the version_number of a version.
"""
if type(version) is int:
version = self.versions.get(version_number=version)
self.active_version = version
if version.rejected:
version.rejected = False
version.save()
def reject_version(self, version):
2013-04-19 14:12:49 +02:00
"""
Reject a version of this motion.
2013-02-03 13:24:29 +01:00
'version' can be a version object, or the version_number of a version.
"""
if type(version) is int:
version = self.versions.get(version_number=version)
if version.active:
raise MotionError('The active version can not be rejected')
version.rejected = True
version.save()
2013-01-06 12:07:37 +01:00
class MotionVersion(models.Model):
2013-02-05 18:46:46 +01:00
"""
2013-04-19 14:12:49 +02:00
A MotionVersion object saves some date of the motion.
"""
2013-02-05 18:46:46 +01:00
motion = models.ForeignKey(Motion, related_name='versions')
"""The motion to which the version belongs."""
2013-02-05 18:46:46 +01:00
2013-02-03 13:24:29 +01:00
version_number = models.PositiveIntegerField(default=1)
2013-02-05 18:46:46 +01:00
"""An id for this version in realation to a motion.
Is unique for each motion.
"""
2013-01-26 16:33:55 +01:00
title = models.CharField(max_length=255, verbose_name=ugettext_lazy("Title"))
"""The title of a motion."""
2013-02-05 18:46:46 +01:00
2013-01-06 12:07:37 +01:00
text = models.TextField(verbose_name=_("Text"))
2013-02-05 18:46:46 +01:00
"""The text of a motion."""
2013-01-26 16:33:55 +01:00
reason = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Reason"))
2013-02-05 18:46:46 +01:00
"""The reason for a motion."""
2013-01-06 12:07:37 +01:00
rejected = models.BooleanField(default=False)
"""Saves if the version is rejected."""
2013-02-05 18:46:46 +01:00
2013-01-06 12:07:37 +01:00
creation_time = models.DateTimeField(auto_now=True)
"""Time when the version was saved."""
2013-02-05 18:46:46 +01:00
#identifier = models.CharField(max_length=255, verbose_name=ugettext_lazy("Version identifier"))
#note = models.TextField(null=True, blank=True)
2013-02-03 13:24:29 +01:00
class Meta:
unique_together = ("motion", "version_number")
2013-01-06 12:07:37 +01:00
def __unicode__(self):
2013-02-05 18:46:46 +01:00
"""Return a string, representing this object."""
2013-02-03 13:24:29 +01:00
counter = self.version_number or _('new')
return "%s Version %s" % (self.motion, counter) # TODO: Should this really be self.motion or the title of the specific version?
2013-01-06 12:07:37 +01:00
2013-01-26 15:25:54 +01:00
def get_absolute_url(self, link='detail'):
2013-02-05 18:46:46 +01:00
"""Return the URL of this Version.
The keyargument link can be 'view' or 'detail'.
"""
2013-01-26 15:25:54 +01:00
if link == 'view' or link == 'detail':
return reverse('motion_version_detail', args=[str(self.motion.id),
str(self.version_number)])
@property
2013-02-03 13:24:29 +01:00
def active(self):
2013-02-05 18:46:46 +01:00
"""Return True, if the version is the active version of a motion. Else: False."""
2013-02-03 13:24:29 +01:00
return self.active_version.exists()
2012-04-12 16:21:30 +02:00
2013-02-05 18:46:46 +01:00
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."""
2013-01-06 12:07:37 +01:00
def __unicode__(self):
2013-02-05 18:46:46 +01:00
"""Return the name of the submitter as string."""
return unicode(self.person)
2013-02-05 18:46:46 +01:00
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)
2013-03-11 20:17:19 +01:00
class Category(models.Model):
name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name"))
"""Name of the category."""
prefix = models.CharField(blank=True, max_length=32, verbose_name=ugettext_lazy("Prefix"))
"""Prefix of the category.
Used to build the identifier of a motion.
"""
2013-03-11 20:17:19 +01:00
def __unicode__(self):
return self.name
2013-02-05 18:46:46 +01:00
2013-03-11 20:17:19 +01:00
def get_absolute_url(self, link='update'):
if link == 'update' or link == 'edit':
return reverse('motion_category_update', args=[str(self.id)])
2013-03-11 21:38:07 +01:00
if link == 'delete':
return reverse('motion_category_delete', args=[str(self.id)])
2013-02-05 18:46:46 +01:00
2013-03-11 21:29:56 +01:00
class Meta:
ordering = ['prefix']
2013-02-05 18:46:46 +01:00
## class Comment(models.Model):
## motion_version = models.ForeignKey(MotionVersion)
## text = models.TextField()
## author = PersonField()
## creation_time = models.DateTimeField(auto_now=True)
2013-02-01 12:51:54 +01:00
class MotionLog(models.Model):
2013-02-05 18:46:46 +01:00
"""Save a logmessage for a motion."""
motion = models.ForeignKey(Motion, related_name='log_messages')
2013-02-05 18:46:46 +01:00
"""The motion to witch the object belongs."""
message = models.CharField(max_length=255) # TODO: arguments in message, not translatable
2013-02-05 18:46:46 +01:00
"""The log message.
Should be in english.
"""
person = PersonField(null=True)
2013-02-05 18:46:46 +01:00
"""A person object, who created the log message. Optional."""
time = models.DateTimeField(auto_now=True)
2013-02-05 18:46:46 +01:00
"""The Time, when the loged action was performed."""
class Meta:
ordering = ['-time']
def __unicode__(self):
2013-02-05 18:46:46 +01:00
"""Return a string, representing the log message."""
time = formats.date_format(self.time, 'DATETIME_FORMAT')
if self.person is None:
return "%s %s" % (time, _(self.message))
else:
return "%s %s by %s" % (time, _(self.message), self.person)
2013-02-03 14:14:07 +01:00
2013-02-01 12:51:54 +01:00
class MotionVote(BaseVote):
2013-02-05 18:46:46 +01:00
"""Saves the votes for a MotionPoll.
There should allways be three MotionVote objects for each poll,
one for 'yes', 'no', and 'abstain'."""
2013-02-01 12:51:54 +01:00
option = models.ForeignKey('MotionOption')
2013-02-05 18:46:46 +01:00
"""The option object, to witch the vote belongs."""
2013-02-01 12:51:54 +01:00
class MotionOption(BaseOption):
2013-02-05 18:46:46 +01:00
"""Links between the MotionPollClass and the MotionVoteClass.
There should be one MotionOption object for each poll."""
2013-02-01 12:51:54 +01:00
poll = models.ForeignKey('MotionPoll')
2013-02-05 18:46:46 +01:00
"""The poll object, to witch the object belongs."""
2013-02-01 12:51:54 +01:00
vote_class = MotionVote
2013-02-05 18:46:46 +01:00
"""The VoteClass, to witch this Class links."""
2013-02-01 12:51:54 +01:00
class MotionPoll(CountInvalid, CountVotesCast, BasePoll):
2013-02-05 18:46:46 +01:00
"""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."""
2013-02-01 12:51:54 +01:00
option_class = MotionOption
2013-02-05 18:46:46 +01:00
"""The option class, witch links between this object the the votes."""
2013-02-01 12:51:54 +01:00
vote_values = [
ugettext_noop('Yes'), ugettext_noop('No'), ugettext_noop('Abstain')]
2013-02-05 18:46:46 +01:00
"""The possible anwers for the poll. 'Yes, 'No' and 'Abstain'."""
2013-02-01 12:51:54 +01:00
poll_number = models.PositiveIntegerField(default=1)
2013-02-05 18:46:46 +01:00
"""An id for this poll in realation to a motion.
Is unique for each motion.
"""
2013-02-01 12:51:54 +01:00
class Meta:
unique_together = ("motion", "poll_number")
def __unicode__(self):
2013-02-05 18:46:46 +01:00
"""Return a string, representing the poll."""
return _('Ballot %d') % self.poll_number
2013-02-01 12:51:54 +01:00
def get_absolute_url(self, link='edit'):
2013-02-05 18:46:46 +01:00
"""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)])
if link == 'delete':
return reverse('motion_poll_delete', args=[str(self.motion.pk),
str(self.poll_number)])
2013-02-01 12:51:54 +01:00
def set_options(self):
2013-02-05 18:46:46 +01:00
"""Create the option class for this poll."""
2013-02-01 12:51:54 +01:00
#TODO: maybe it is possible with .create() to call this without poll=self
2013-02-05 18:46:46 +01:00
# or call this in save()
2013-02-01 12:51:54 +01:00
self.get_option_class()(poll=self).save()
def append_pollform_fields(self, fields):
2013-02-05 18:46:46 +01:00
"""Apend the fields for invalid and votecast to the ModelForm."""
2013-02-01 12:51:54 +01:00
CountInvalid.append_pollform_fields(self, fields)
CountVotesCast.append_pollform_fields(self, fields)
class State(models.Model):
"""Defines a state for a motion.
Every state belongs to a workflow. All states of a workflow are linked together
via 'next_states'. One of these states is the first state, but this
is saved in the workflow table (one-to-one relation). In every state
you can configure some handling of a motion. See the following fields
for more information.
"""
name = models.CharField(max_length=255)
"""A string representing the state."""
action_word = models.CharField(max_length=255)
"""An alternative string to be used for a button to switch to this state."""
workflow = models.ForeignKey('Workflow')
"""A many-to-one relation to a workflow."""
next_states = models.ManyToManyField('self', symmetrical=False)
"""A many-to-many relation to all states, that can be choosen from this state."""
icon = models.CharField(max_length=255)
"""A string representing the url to the icon-image."""
allow_support = models.BooleanField(default=False)
"""If true, persons can support the motion in this state."""
allow_create_poll = models.BooleanField(default=False)
"""If true, polls can be created in this state."""
allow_submitter_edit = models.BooleanField(default=False)
"""If true, the submitter can edit the motion in this state."""
versioning = models.BooleanField(default=False)
"""
If true, editing the motion will create a new version by default.
This behavior can be changed by the form and view, e. g. via the
MotionDisableVersioningMixin.
"""
leave_old_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."""
2013-03-12 22:03:56 +01:00
def __unicode__(self):
"""Returns the name of the state."""
return self.name
def save(self, **kwargs):
"""Saves a state to the database.
Used to check the integrity before saving.
"""
self.check_next_states()
super(State, self).save(**kwargs)
def get_action_word(self):
"""Returns the alternative name of the state if it exists."""
return self.action_word or self.name
def check_next_states(self):
"""Checks whether all next states of a state belong to the correct workflow."""
# No check if it is a new state which has not been saved yet.
if not self.id:
return
for state in self.next_states.all():
if not state.workflow == self.workflow:
raise WorkflowError('%s can not be next state of %s because it does not belong to the same workflow.' % (state, self))
class Workflow(models.Model):
"""Defines a workflow for a motion."""
name = models.CharField(max_length=255)
"""A string representing the workflow."""
first_state = models.OneToOneField(State, related_name='+', null=True)
"""A one-to-one relation to a state, the starting point for the workflow."""
def __unicode__(self):
"""Returns the name of the workflow."""
return self.name
def save(self, **kwargs):
"""Saves a workflow to the database.
Used to check the integrity before saving.
"""
self.check_first_state()
super(Workflow, self).save(**kwargs)
def check_first_state(self):
"""Checks whether the first_state itself belongs to the workflow."""
if self.first_state and not self.first_state.workflow == self:
raise WorkflowError('%s can not be first state of %s because it does not belong to it.' % (self.first_state, self))