36f83808fd
The motion import now features an additional column to indicate that the import should be created by an existing group. If the column is missing it is considered as 'off' and first / last name are used as always. If the column exists and contains 'y','yes','j','ja','true', '1' or 1 first name will be ignored and last name will be interpreted as a group name. If the group exists and can act as a person the motion will be imported and assigned to this group. Signed-off-by: René Köcher <shirk@bitspin.org>
613 lines
20 KiB
Python
613 lines
20 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
openslides.motion.models
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Models for the motion app.
|
|
|
|
:copyright: 2011, 2012 by OpenSlides team, see AUTHORS.
|
|
:license: GNU GPL, see LICENSE for more details.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from django.core.urlresolvers import reverse
|
|
from django.db import models
|
|
from django.db.models import Max
|
|
from django.dispatch import receiver
|
|
from django.utils.translation import pgettext
|
|
from django.utils.translation import ugettext_lazy as _, ugettext_noop, ugettext
|
|
|
|
from openslides.utils.utils import _propper_unicode
|
|
from openslides.utils.person import PersonField
|
|
|
|
from openslides.config.models import config
|
|
from openslides.config.signals import default_config_value
|
|
|
|
from openslides.poll.models import (BaseOption, BasePoll, CountVotesCast,
|
|
CountInvalid, BaseVote)
|
|
|
|
from openslides.participant.models import User, Group
|
|
|
|
from openslides.projector.api import register_slidemodel
|
|
from openslides.projector.models import SlideMixin
|
|
|
|
from openslides.agenda.models import Item
|
|
|
|
|
|
class MotionSupporter(models.Model):
|
|
motion = models.ForeignKey("Motion")
|
|
person = PersonField()
|
|
|
|
|
|
class Motion(models.Model, SlideMixin):
|
|
prefix = "motion"
|
|
STATUS = (
|
|
('pub', _('Published')),
|
|
('per', _('Permitted')),
|
|
('acc', _('Accepted')),
|
|
('rej', _('Rejected')),
|
|
('wit', _('Withdrawed')),
|
|
('adj', _('Adjourned')),
|
|
('noc', _('Not Concerned')),
|
|
('com', _('Commited a bill')),
|
|
('nop', _('Rejected (not authorized)')),
|
|
('rev', _('Needs Review')), # Where is this status used?
|
|
#additional actions:
|
|
# edit
|
|
# delete
|
|
# setnumber
|
|
# support
|
|
# unsupport
|
|
# createitem
|
|
# activateitem
|
|
# genpoll
|
|
)
|
|
|
|
submitter = PersonField(verbose_name=_("Submitter"))
|
|
number = models.PositiveSmallIntegerField(blank=True, null=True,
|
|
unique=True)
|
|
status = models.CharField(max_length=3, choices=STATUS, default='pub')
|
|
permitted = models.ForeignKey('AVersion', related_name='permitted', \
|
|
null=True, blank=True)
|
|
log = models.TextField(blank=True, null=True)
|
|
|
|
@property
|
|
def last_version(self):
|
|
"""
|
|
Return last version of the motion.
|
|
"""
|
|
try:
|
|
return AVersion.objects.filter(motion=self).order_by('id') \
|
|
.reverse()[0]
|
|
except IndexError:
|
|
return None
|
|
|
|
@property
|
|
def public_version(self):
|
|
"""
|
|
Return permitted, if the motion was permitted, else last_version
|
|
"""
|
|
if self.permitted is not None:
|
|
return self.permitted
|
|
else:
|
|
return self.last_version
|
|
|
|
def accept_version(self, version, user = None):
|
|
"""
|
|
accept a Version
|
|
"""
|
|
self.permitted = version
|
|
self.save(nonewversion=True)
|
|
version.rejected = False
|
|
version.save()
|
|
self.writelog(_("Version %d authorized") % (version.aid, ),
|
|
user)
|
|
|
|
def reject_version(self, version, user = None):
|
|
if version.id > self.permitted.id:
|
|
version.rejected = True
|
|
version.save()
|
|
self.writelog(pgettext("Rejected means not authorized", "Version %d rejected")
|
|
% (version.aid, ), user)
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def versions(self):
|
|
"""
|
|
Return a list of all versions of the motion.
|
|
"""
|
|
return AVersion.objects.filter(motion=self)
|
|
|
|
@property
|
|
def creation_time(self):
|
|
"""
|
|
Return the time of the creation of the motion.
|
|
"""
|
|
try:
|
|
return self.versions[0].time
|
|
except IndexError:
|
|
return None
|
|
|
|
@property
|
|
def notes(self):
|
|
"""
|
|
Return some information of the motion.
|
|
"""
|
|
note = []
|
|
if self.status == "pub" and not self.enough_supporters:
|
|
note.append(ugettext("Searching for supporters."))
|
|
if self.status == "pub" and self.permitted is None:
|
|
note.append(ugettext("Not yet authorized."))
|
|
elif self.unpermitted_changes and self.permitted:
|
|
note.append(ugettext("Not yet authorized changes."))
|
|
return note
|
|
|
|
@property
|
|
def unpermitted_changes(self):
|
|
"""
|
|
Return True if the motion has unpermitted changes.
|
|
|
|
The motion has unpermitted changes, if the permitted-version
|
|
is not the lastone and the lastone is not rejected.
|
|
TODO: rename the property in unchecked__changes
|
|
"""
|
|
if (self.last_version != self.permitted
|
|
and not self.last_version.rejected):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@property
|
|
def supporters(self):
|
|
for object in self.motionsupporter_set.all():
|
|
yield object.person
|
|
|
|
def is_supporter(self, person):
|
|
try:
|
|
return self.motionsupporter_set.filter(person=person).exists()
|
|
except AttributeError:
|
|
return False
|
|
|
|
@property
|
|
def enough_supporters(self):
|
|
"""
|
|
Return True, if the motion has enough supporters
|
|
"""
|
|
min_supporters = int(config['motion_min_supporters'])
|
|
if self.status == "pub":
|
|
return self.count_supporters() >= min_supporters
|
|
else:
|
|
return True
|
|
|
|
def count_supporters(self):
|
|
return self.motionsupporter_set.count()
|
|
|
|
@property
|
|
def missing_supporters(self):
|
|
"""
|
|
Return number of missing supporters
|
|
"""
|
|
min_supporters = int(config['motion_min_supporters'])
|
|
delta = min_supporters - self.count_supporters()
|
|
if delta > 0:
|
|
return delta
|
|
else:
|
|
return 0
|
|
|
|
def save(self, user=None, nonewversion=False, trivial_change=False):
|
|
"""
|
|
Save the Motion, and create a new AVersion if necessary
|
|
"""
|
|
super(Motion, self).save()
|
|
if nonewversion:
|
|
return
|
|
last_version = self.last_version
|
|
fields = ["text", "title", "reason"]
|
|
if last_version is not None:
|
|
changed_fields = [f for f in fields
|
|
if getattr(last_version, f) != getattr(self, f)]
|
|
if not changed_fields:
|
|
return # No changes
|
|
try:
|
|
if trivial_change and last_version is not None:
|
|
last_version.text = self.text
|
|
last_version.title = self.title
|
|
last_version.reason = self.reason
|
|
last_version.save()
|
|
|
|
meta = AVersion._meta
|
|
field_names = [unicode(meta.get_field(f).verbose_name)
|
|
for f in changed_fields]
|
|
|
|
self.writelog(_("Trivial changes to version %(version)d; "
|
|
"changed fields: %(changed_fields)s")
|
|
% dict(version = last_version.aid,
|
|
changed_fields = ", ".join(field_names)))
|
|
return # Done
|
|
|
|
version = AVersion(title=getattr(self, 'title', ''),
|
|
text=getattr(self, 'text', ''),
|
|
reason=getattr(self, 'reason', ''),
|
|
motion=self)
|
|
version.save()
|
|
self.writelog(_("Version %s created") % version.aid, user)
|
|
is_manager = user.has_perm('motion.can_manage_motion')
|
|
except AttributeError:
|
|
is_manager = False
|
|
|
|
supporters = self.motionsupporter_set.all()
|
|
if (self.status == "pub"
|
|
and supporters
|
|
and not is_manager):
|
|
supporters.delete()
|
|
self.writelog(_("Supporters removed"), user)
|
|
|
|
def reset(self, user):
|
|
"""
|
|
Reset the motion.
|
|
"""
|
|
self.status = "pub"
|
|
self.permitted = None
|
|
self.save()
|
|
self.writelog(_("Status reseted to: %s") % (self.get_status_display()), user)
|
|
|
|
def support(self, person):
|
|
"""
|
|
Add a Supporter to the list of supporters of the motion.
|
|
"""
|
|
if person == self.submitter:
|
|
# TODO: Use own Exception
|
|
raise NameError('Supporter can not be the submitter of a '
|
|
'motion.')
|
|
if not self.is_supporter(person):
|
|
MotionSupporter(motion=self, person=person).save()
|
|
self.writelog(_("Supporter: +%s") % (person))
|
|
# TODO: Raise a precise exception for the view in else-clause
|
|
|
|
def unsupport(self, person):
|
|
"""
|
|
remove a supporter from the list of supporters of the motion
|
|
"""
|
|
try:
|
|
object = self.motionsupporter_set.get(person=person).delete()
|
|
except MotionSupporter.DoesNotExist:
|
|
# TODO: Don't do nothing but raise a precise exception for the view
|
|
pass
|
|
else:
|
|
self.writelog(_("Supporter: -%s") % (person))
|
|
|
|
def set_number(self, number=None, user=None):
|
|
"""
|
|
Set a number for ths motion.
|
|
"""
|
|
if self.number is not None:
|
|
# TODO: Use own Exception
|
|
raise NameError('This motion has already a number.')
|
|
if number is None:
|
|
try:
|
|
number = Motion.objects.aggregate(Max('number')) \
|
|
['number__max'] + 1
|
|
except TypeError:
|
|
number = 1
|
|
self.number = number
|
|
self.save()
|
|
self.writelog(_("Number set: %s") % (self.number), user)
|
|
return self.number
|
|
|
|
def permit(self, user=None):
|
|
"""
|
|
Change the status of this motion to permit.
|
|
"""
|
|
self.set_status(user, "per")
|
|
aversion = self.last_version
|
|
if self.number is None:
|
|
self.set_number()
|
|
self.permitted = aversion
|
|
self.save()
|
|
self.writelog(_("Version %s authorized") % (aversion.aid), user)
|
|
return self.permitted
|
|
|
|
def notpermit(self, user=None):
|
|
"""
|
|
Change the status of this motion to 'not permitted (rejected)'.
|
|
"""
|
|
self.set_status(user, "nop")
|
|
#TODO: reject last version
|
|
aversion = self.last_version
|
|
#self.permitted = aversion
|
|
if self.number is None:
|
|
self.set_number()
|
|
self.save()
|
|
self.writelog(_("Version %s not authorized") % (self.last_version.aid), user)
|
|
|
|
def set_status(self, user, status, force=False):
|
|
"""
|
|
Set the status of the motion.
|
|
"""
|
|
error = True
|
|
for a, b in Motion.STATUS:
|
|
if status == a:
|
|
error = False
|
|
break
|
|
if error:
|
|
#TODO: Use the Right Error
|
|
raise NameError(_('%s is not a valid status.') % status)
|
|
if self.status == status:
|
|
#TODO: Use the Right Error
|
|
raise NameError(_('The motion status is already \'%s.\'') \
|
|
% self.status)
|
|
|
|
actions = []
|
|
actions = self.get_allowed_actions(user)
|
|
if status not in actions and not force:
|
|
#TODO: Use the Right Error
|
|
raise NameError(_(
|
|
'The motion status is: \'%(currentstatus)s\'. '
|
|
'You can not set the status to \'%(newstatus)s\'.') % {
|
|
'currentstatus': self.status,
|
|
'newstatus': status})
|
|
|
|
oldstatus = self.get_status_display()
|
|
self.status = status
|
|
self.save()
|
|
self.writelog(_("Status modified")+": %s -> %s" \
|
|
% (oldstatus, self.get_status_display()), user)
|
|
|
|
def get_allowed_actions(self, user):
|
|
"""
|
|
Return a list of all the allowed status.
|
|
"""
|
|
actions = []
|
|
|
|
# check if user allowed to withdraw an motion
|
|
if ((self.status == "pub"
|
|
and self.number
|
|
and user == self.submitter)
|
|
or (self.status == "pub"
|
|
and self.number
|
|
and user.has_perm("motion.can_manage_motion"))
|
|
or (self.status == "per"
|
|
and user == self.submitter)
|
|
or (self.status == "per"
|
|
and user.has_perm("motion.can_manage_motion"))):
|
|
actions.append("wit")
|
|
#Check if the user can review the motion
|
|
if (self.status == "rev"
|
|
and (self.submitter == user
|
|
or user.has_perm("motion.can_manage_motion"))):
|
|
actions.append("pub")
|
|
|
|
# Check if the user can support and unspoort the motion
|
|
if (self.status == "pub"
|
|
and user != self.submitter
|
|
and not self.is_supporter(user)):
|
|
actions.append("support")
|
|
|
|
if self.status == "pub" and self.is_supporter(user):
|
|
actions.append("unsupport")
|
|
|
|
#Check if the user can edit the motion
|
|
if (user == self.submitter \
|
|
and (self.status in ('pub', 'per'))) \
|
|
or user.has_perm("motion.can_manage_motion"):
|
|
actions.append("edit")
|
|
|
|
# Check if the user can delete the motion (admin, manager, owner)
|
|
# reworked as requiered in #100
|
|
if (user.has_perm("motion.can_delete_all_motions") or
|
|
(user.has_perm("motion.can_manage_motion") and
|
|
self.number is None) or
|
|
(self.submitter == user and self.number is None)):
|
|
actions.append("delete")
|
|
|
|
#For the rest, all actions need the manage permission
|
|
if not user.has_perm("motion.can_manage_motion"):
|
|
return actions
|
|
|
|
if self.status == "pub":
|
|
actions.append("nop")
|
|
actions.append("per")
|
|
if self.number == None:
|
|
actions.append("setnumber")
|
|
|
|
if self.status == "per":
|
|
actions.append("acc")
|
|
actions.append("rej")
|
|
actions.append("adj")
|
|
actions.append("noc")
|
|
actions.append("com")
|
|
actions.append("genpoll")
|
|
if self.unpermitted_changes:
|
|
actions.append("permitversion")
|
|
actions.append("rejectversion")
|
|
|
|
return actions
|
|
|
|
def delete(self, force=False):
|
|
"""
|
|
Delete the motion. It is not possible, if the motion has
|
|
allready a number
|
|
"""
|
|
if self.number and not force:
|
|
raise NameError('The motion has already a number. ' \
|
|
'You can not delete it.')
|
|
|
|
|
|
for item in Item.objects.filter(related_sid=self.sid):
|
|
item.delete()
|
|
super(Motion, self).delete()
|
|
|
|
def writelog(self, text, user=None):
|
|
if not self.log:
|
|
self.log = ""
|
|
self.log += u"%s | %s" % (datetime.now().strftime("%d.%m.%Y %H:%M:%S"), _propper_unicode(text))
|
|
if user is not None:
|
|
if isinstance(user, User):
|
|
self.log += u" (%s %s)" % (_("by"), _propper_unicode(user.username))
|
|
else:
|
|
self.log += u" (%s %s)" % (_("by"), _propper_unicode(str(user)))
|
|
self.log += "\n"
|
|
self.save()
|
|
|
|
def get_agenda_title(self):
|
|
return self.public_version.title
|
|
|
|
def get_agenda_title_supplement(self):
|
|
number = self.number or '<i>[%s]</i>' % ugettext('no number')
|
|
return '(%s %s)' % (ugettext('motion'), number)
|
|
|
|
def __getattr__(self, name):
|
|
"""
|
|
if name is title, text, reason or time,
|
|
Return this attribute from the newest version of the motion
|
|
"""
|
|
if name in ('title', 'text', 'reason', 'time', 'aid'):
|
|
try:
|
|
if name == 'aid':
|
|
return self.last_version.aid
|
|
return self.last_version.__dict__[name]
|
|
except TypeError:
|
|
raise AttributeError(name)
|
|
except AttributeError:
|
|
raise AttributeError(name)
|
|
raise AttributeError(name)
|
|
|
|
def gen_poll(self, user=None):
|
|
"""
|
|
Generates a poll object for the motion
|
|
"""
|
|
poll = MotionPoll(motion=self)
|
|
poll.save()
|
|
poll.set_options()
|
|
self.writelog(_("Poll created"), user)
|
|
return poll
|
|
|
|
@property
|
|
def polls(self):
|
|
return self.motionpoll_set.all()
|
|
|
|
@property
|
|
def results(self):
|
|
return self.get_poll_results()
|
|
|
|
def get_poll_results(self):
|
|
"""
|
|
Return a list of voting results
|
|
"""
|
|
results = []
|
|
for poll in self.polls:
|
|
for option in poll.get_options():
|
|
if option.get_votes().exists():
|
|
results.append((option['Yes'], option['No'],
|
|
option['Abstain'], poll.print_votesinvalid(),
|
|
poll.print_votescast()))
|
|
return results
|
|
|
|
|
|
def slide(self):
|
|
"""
|
|
return the slide dict
|
|
"""
|
|
data = super(Motion, self).slide()
|
|
data['motion'] = self
|
|
data['title'] = self.title
|
|
data['template'] = 'projector/Motion.html'
|
|
return data
|
|
|
|
def get_absolute_url(self, link='view'):
|
|
if link == 'view':
|
|
return reverse('motion_view', args=[str(self.id)])
|
|
if link == 'edit':
|
|
return reverse('motion_edit', args=[str(self.id)])
|
|
if link == 'delete':
|
|
return reverse('motion_delete', args=[str(self.id)])
|
|
|
|
def __unicode__(self):
|
|
try:
|
|
return self.last_version.title
|
|
except AttributeError:
|
|
return "no title jet"
|
|
|
|
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")),
|
|
)
|
|
ordering = ('number',)
|
|
|
|
|
|
class AVersion(models.Model):
|
|
title = models.CharField(max_length=100, verbose_name = _("Title"))
|
|
text = models.TextField(verbose_name = _("Text"))
|
|
reason = models.TextField(null=True, blank=True, verbose_name = _("Reason"))
|
|
rejected = models.BooleanField() # = Not Permitted
|
|
time = models.DateTimeField(auto_now=True)
|
|
motion = models.ForeignKey(Motion)
|
|
|
|
def __unicode__(self):
|
|
return "%s %s" % (self.id, self.title)
|
|
|
|
@property
|
|
def aid(self):
|
|
try:
|
|
return self._aid
|
|
except AttributeError:
|
|
self._aid = AVersion.objects \
|
|
.filter(motion=self.motion) \
|
|
.filter(id__lte=self.id).count()
|
|
return self._aid
|
|
|
|
register_slidemodel(Motion)
|
|
|
|
|
|
class MotionVote(BaseVote):
|
|
option = models.ForeignKey('MotionOption')
|
|
|
|
|
|
class MotionOption(BaseOption):
|
|
poll = models.ForeignKey('MotionPoll')
|
|
vote_class = MotionVote
|
|
|
|
|
|
class MotionPoll(BasePoll, CountInvalid, CountVotesCast):
|
|
option_class = MotionOption
|
|
vote_values = [ugettext_noop('Yes'), ugettext_noop('No'),
|
|
ugettext_noop('Abstain')]
|
|
|
|
motion = models.ForeignKey(Motion)
|
|
|
|
def get_motion(self):
|
|
return self.motion
|
|
|
|
def set_options(self):
|
|
#TODO: maybe it is possible with .create() to call this without poll=self
|
|
self.get_option_class()(poll=self).save()
|
|
|
|
def append_pollform_fields(self, fields):
|
|
CountInvalid.append_pollform_fields(self, fields)
|
|
CountVotesCast.append_pollform_fields(self, fields)
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('motion_poll_view', args=[self.id])
|
|
|
|
def get_ballot(self):
|
|
return self.motion.motionpoll_set.filter(id__lte=self.id).count()
|
|
|
|
|
|
@receiver(default_config_value, dispatch_uid="motion_default_config")
|
|
def default_config(sender, key, **kwargs):
|
|
return {
|
|
'motion_min_supporters': 0,
|
|
'motion_preamble': _('The assembly may decide,'),
|
|
'motion_pdf_ballot_papers_selection': 'CUSTOM_NUMBER',
|
|
'motion_pdf_ballot_papers_number': '8',
|
|
'motion_pdf_title': _('Motions'),
|
|
'motion_pdf_preamble': '',
|
|
'motion_allow_trivial_change': False,
|
|
}.get(key)
|