Merge pull request #600 from ostcar/motion

Fixed motion bugs
This commit is contained in:
Oskar Hahn 2013-04-19 11:00:39 -07:00
commit 38558e9b03
6 changed files with 187 additions and 58 deletions

View File

@ -36,17 +36,21 @@ from .exceptions import MotionError, WorkflowError
class Motion(SlideMixin, models.Model): class Motion(SlideMixin, models.Model):
"""The Motion Class. """
The Motion Class.
This class is the main entry point to all other classes related to a motion. This class is the main entry point to all other classes related to a motion.
""" """
prefix = 'motion' prefix = 'motion'
"""Prefix for the slide system.""" """
Prefix for the slide system.
"""
active_version = models.ForeignKey('MotionVersion', null=True, active_version = models.ForeignKey('MotionVersion', null=True,
related_name="active_version") related_name="active_version")
"""Points to a specific version. """
Points to a specific version.
Used be the permitted-version-system to deside which version is the active 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. Could also be used to only choose a specific version as a default
@ -54,23 +58,29 @@ class Motion(SlideMixin, models.Model):
""" """
state = models.ForeignKey('State', null=True) # TODO: Check whether null=True is necessary. state = models.ForeignKey('State', null=True) # TODO: Check whether null=True is necessary.
"""The related state object. """
The related state object.
This attribute is to get the current state of the motion. This attribute is to get the current state of the motion.
""" """
identifier = models.CharField(max_length=255, null=True, blank=True, identifier = models.CharField(max_length=255, null=True, blank=True,
unique=True) unique=True)
"""A string as human readable identifier for the motion.""" """
A string as human readable identifier for the motion.
"""
identifier_number = models.IntegerField(null=True) identifier_number = models.IntegerField(null=True)
"""Counts the number of the motion in one category. """
Counts the number of the motion in one category.
Needed to find the next free motion-identifier. Needed to find the next free motion-identifier.
""" """
category = models.ForeignKey('Category', null=True, blank=True) category = models.ForeignKey('Category', null=True, blank=True)
"""ForeignKey to one category of motions.""" """
ForeignKey to one category of motions.
"""
# TODO: proposal # TODO: proposal
#master = models.ForeignKey('self', null=True, blank=True) #master = models.ForeignKey('self', null=True, blank=True)
@ -86,12 +96,15 @@ class Motion(SlideMixin, models.Model):
# ordering = ('number',) # ordering = ('number',)
def __unicode__(self): def __unicode__(self):
"""Return a human readable name of this motion.""" """
Return a human readable name of this motion.
"""
return self.get_title() return self.get_title()
# TODO: Use transaction # TODO: Use transaction
def save(self, *args, **kwargs): def save(self, no_new_version=False, *args, **kwargs):
"""Save the motion. """
Save the motion.
1. Set the state of a new motion to the default state. 1. Set the state of a new motion to the default state.
2. Save the motion object. 2. Save the motion object.
@ -109,12 +122,20 @@ class Motion(SlideMixin, models.Model):
the config 'motion_create_new_version' is set to the config 'motion_create_new_version' is set to
'ALWAYS_CREATE_NEW_VERSION'. 'ALWAYS_CREATE_NEW_VERSION'.
If no_new_version is True, a new version will never be used.
""" """
if not self.state: if not self.state:
self.reset_state() self.reset_state()
if not self.identifier and self.identifier is not None:
self.identifier = None
super(Motion, self).save(*args, **kwargs) super(Motion, self).save(*args, **kwargs)
if no_new_version:
return
# Find out if the version data has changed # Find out if the version data has changed
for attr in ['title', 'text', 'reason']: for attr in ['title', 'text', 'reason']:
if not self.versions.exists(): if not self.versions.exists():
@ -126,7 +147,8 @@ class Motion(SlideMixin, models.Model):
else: else:
new_data = False new_data = False
# TODO: Check everything here. The decision whether to create a new version has to be done in the view. Update docstings too. # 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 need_new_version = self.state.versioning
if hasattr(self, '_new_version') or (new_data and need_new_version): if hasattr(self, '_new_version') or (new_data and need_new_version):
version = self.new_version version = self.new_version
@ -163,7 +185,8 @@ class Motion(SlideMixin, models.Model):
self.save() self.save()
def get_absolute_url(self, link='detail'): def get_absolute_url(self, link='detail'):
"""Return an URL for this version. """
Return an URL for this version.
The keyword argument 'link' can be 'detail', 'view', 'edit' or 'delete'. The keyword argument 'link' can be 'detail', 'view', 'edit' or 'delete'.
""" """
@ -202,7 +225,8 @@ class Motion(SlideMixin, models.Model):
break break
def get_title(self): def get_title(self):
"""Get the title of the motion. """
Get the title of the motion.
The titel is taken from motion.version. The titel is taken from motion.version.
""" """
@ -212,7 +236,8 @@ class Motion(SlideMixin, models.Model):
return self.version.title return self.version.title
def set_title(self, title): def set_title(self, title):
"""Set the titel of the motion. """
Set the titel of the motion.
The titel will me saved into the version object, wenn motion.save() is The titel will me saved into the version object, wenn motion.save() is
called. called.
@ -220,13 +245,15 @@ class Motion(SlideMixin, models.Model):
self._title = title self._title = title
title = property(get_title, set_title) title = property(get_title, set_title)
"""The title of the motion. """
The title of the motion.
Is saved in a MotionVersion object. Is saved in a MotionVersion object.
""" """
def get_text(self): def get_text(self):
"""Get the text of the motion. """
Get the text of the motion.
Simular to get_title(). Simular to get_title().
""" """
@ -236,20 +263,23 @@ class Motion(SlideMixin, models.Model):
return self.version.text return self.version.text
def set_text(self, text): def set_text(self, text):
""" Set the text of the motion. """
Set the text of the motion.
Simular to set_title(). Simular to set_title().
""" """
self._text = text self._text = text
text = property(get_text, set_text) text = property(get_text, set_text)
"""The text of a motin. """
The text of a motin.
Is saved in a MotionVersion object. Is saved in a MotionVersion object.
""" """
def get_reason(self): def get_reason(self):
"""Get the reason of the motion. """
Get the reason of the motion.
Simular to get_title(). Simular to get_title().
""" """
@ -259,21 +289,24 @@ class Motion(SlideMixin, models.Model):
return self.version.reason return self.version.reason
def set_reason(self, reason): def set_reason(self, reason):
"""Set the reason of the motion. """
Set the reason of the motion.
Simular to set_title(). Simular to set_title().
""" """
self._reason = reason self._reason = reason
reason = property(get_reason, set_reason) reason = property(get_reason, set_reason)
"""The reason for the motion. """
The reason for the motion.
Is saved in a MotionVersion object. Is saved in a MotionVersion object.
""" """
@property @property
def new_version(self): def new_version(self):
"""Return a version object, not saved in the database. """
Return a version object, not saved in the database.
On the first call, it creates a new version. On any later call, it On the first call, it creates a new version. On any later call, it
use the existing new version. use the existing new version.
@ -287,7 +320,8 @@ class Motion(SlideMixin, models.Model):
return self._new_version return self._new_version
def get_version(self): def get_version(self):
"""Get the 'active' version object. """
Get the 'active' version object.
This version will be used to get the data for this motion. This version will be used to get the data for this motion.
""" """
@ -297,7 +331,8 @@ class Motion(SlideMixin, models.Model):
return self.last_version return self.last_version
def set_version(self, version): def set_version(self, version):
"""Set the 'active' version object. """
Set the 'active' version object.
The keyword argument 'version' can be a MotionVersion object or the The keyword argument 'version' can be a MotionVersion object or the
version_number of a version object or None. version_number of a version object or None.
@ -319,11 +354,15 @@ class Motion(SlideMixin, models.Model):
self._version = version self._version = version
version = property(get_version, set_version) version = property(get_version, set_version)
"""The active version of this motion.""" """
The active version of this motion.
"""
@property @property
def last_version(self): def last_version(self):
"""Return the newest version of the motion.""" """
Return the newest version of the motion.
"""
# TODO: Fix the case, that the motion has no version # TODO: Fix the case, that the motion has no version
try: try:
return self.versions.order_by('-version_number')[0] return self.versions.order_by('-version_number')[0]
@ -348,11 +387,15 @@ class Motion(SlideMixin, models.Model):
MotionSubmitter.objects.create(motion=self, person=person) MotionSubmitter.objects.create(motion=self, person=person)
def is_supporter(self, person): def is_supporter(self, person):
"""Return True, if person is a supporter of this motion. Else: False.""" """
Return True, if person is a supporter of this motion. Else: False.
"""
return self.supporter.filter(person=person).exists() return self.supporter.filter(person=person).exists()
def support(self, person): def support(self, person):
"""Add 'person' as a supporter of this motion.""" """
Add 'person' as a supporter of this motion.
"""
if self.state.allow_support: if self.state.allow_support:
if not self.is_supporter(person): if not self.is_supporter(person):
MotionSupporter(motion=self, person=person).save() MotionSupporter(motion=self, person=person).save()
@ -360,14 +403,17 @@ class Motion(SlideMixin, models.Model):
raise WorkflowError('You can not support a motion in state %s.' % self.state.name) raise WorkflowError('You can not support a motion in state %s.' % self.state.name)
def unsupport(self, person): def unsupport(self, person):
"""Remove 'person' as supporter from this motion.""" """
Remove 'person' as supporter from this motion.
"""
if self.state.allow_support: if self.state.allow_support:
self.supporter.filter(person=person).delete() self.supporter.filter(person=person).delete()
else: else:
raise WorkflowError('You can not unsupport a motion in state %s.' % self.state.name) raise WorkflowError('You can not unsupport a motion in state %s.' % self.state.name)
def create_poll(self): def create_poll(self):
"""Create a new poll for this motion. """
Create a new poll for this motion.
Return the new poll object. Return the new poll object.
""" """
@ -381,7 +427,8 @@ class Motion(SlideMixin, models.Model):
raise WorkflowError('You can not create a poll in state %s.' % self.state.name) raise WorkflowError('You can not create a poll in state %s.' % self.state.name)
def set_state(self, state): def set_state(self, state):
"""Set the state of the motion. """
Set the state of the motion.
State can be the id of a state object or a state object. State can be the id of a state object or a state object.
""" """
@ -393,7 +440,11 @@ class Motion(SlideMixin, models.Model):
self.state = state self.state = state
def reset_state(self): def reset_state(self):
"""Set the state to the default state. If the motion is new, it chooses the default workflow from config.""" """
Set the state to the default state.
If the motion is new, it chooses the default workflow from config.
"""
if self.state: if self.state:
self.state = self.state.workflow.first_state self.state = self.state.workflow.first_state
else: else:
@ -401,7 +452,9 @@ class Motion(SlideMixin, models.Model):
Workflow.objects.get(pk=config['motion_workflow']).state_set.all()[0]) Workflow.objects.get(pk=config['motion_workflow']).state_set.all()[0])
def slide(self): def slide(self):
"""Return the slide dict.""" """
Return the slide dict.
"""
data = super(Motion, self).slide() data = super(Motion, self).slide()
data['motion'] = self data['motion'] = self
data['title'] = self.title data['title'] = self.title
@ -409,7 +462,9 @@ class Motion(SlideMixin, models.Model):
return data return data
def get_agenda_title(self): def get_agenda_title(self):
"""Return a title for the Agenda.""" """
Return a title for the Agenda.
"""
return self.last_version.title # TODO: nutze active_version return self.last_version.title # TODO: nutze active_version
## def get_agenda_title_supplement(self): ## def get_agenda_title_supplement(self):
@ -417,7 +472,8 @@ class Motion(SlideMixin, models.Model):
## return '(%s %s)' % (ugettext('motion'), number) ## return '(%s %s)' % (ugettext('motion'), number)
def get_allowed_actions(self, person): def get_allowed_actions(self, person):
"""Return 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. The dictonary contains the following actions.
@ -462,8 +518,9 @@ class Motion(SlideMixin, models.Model):
""" """
MotionLog.objects.create(motion=self, message=message, person=person) MotionLog.objects.create(motion=self, message=message, person=person)
def activate_version(self, version): def set_active_version(self, version):
"""Set the active state of a version to True. """
Set the active state of a version to 'version'.
'version' can be a version object, or the version_number of a version. 'version' can be a version object, or the version_number of a version.
""" """
@ -476,7 +533,8 @@ class Motion(SlideMixin, models.Model):
version.save() version.save()
def reject_version(self, version): 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. 'version' can be a version object, or the version_number of a version.
""" """
@ -492,7 +550,8 @@ class Motion(SlideMixin, models.Model):
class MotionVersion(models.Model): class MotionVersion(models.Model):
""" """
A MotionVersion object saves some date of the motion.""" A MotionVersion object saves some date of the motion.
"""
motion = models.ForeignKey(Motion, related_name='versions') motion = models.ForeignKey(Motion, related_name='versions')
"""The motion to which the version belongs.""" """The motion to which the version belongs."""

View File

@ -4,15 +4,15 @@
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load staticfiles %}
{% block title %}{{ block.super }} {% trans "Motion" %} {{ motion.identifier }}{% endblock %} {% block title %}{{ block.super }} {% trans "Motion" %} {{ motion.identifier|default:'' }}{% endblock %}
{% block content %} {% block content %}
<h1> <h1>
{{ motion.title }} {{ motion.category }} {{ motion.title }} {{ motion.category|default:'' }}
<br> <br>
<small> <small>
{% if motion.identifier %} {% if motion.identifier %}
{% trans "Motion" %} {{ motion.identifier }}, {% trans "Motion" %} {{ motion.identifier|default:'' }},
{% else %} {% else %}
<i>[{% trans "no number" %}]</i>, <i>[{% trans "no number" %}]</i>,
{% endif %} {% endif %}

View File

@ -63,7 +63,7 @@
</tr> </tr>
{% for motion in motion_list %} {% for motion in motion_list %}
<tr class="{% if motion.active %}activeline{% endif %}"> <tr class="{% if motion.active %}activeline{% endif %}">
<td>{{ motion.identifier }}</td> <td>{{ motion.identifier|default:'' }}</td>
<td><a href="{% model_url motion %}">{{ motion.title }}</a></td> <td><a href="{% model_url motion %}">{{ motion.title }}</a></td>
{% if min_supporters > 0 %} {% if min_supporters > 0 %}
<td class="optional">{# motion.count_supporters #}</td> <td class="optional">{# motion.count_supporters #}</td>

View File

@ -83,21 +83,22 @@ motion_detail = MotionDetailView.as_view()
class MotionMixin(object): class MotionMixin(object):
"""Mixin for MotionViewsClasses to save the version data.""" """
Mixin for MotionViewsClasses to save the version data.
"""
def manipulate_object(self, form): def manipulate_object(self, form):
"""Save the version data into the motion object before it is saved in """
the Database.""" Save the version data into the motion object before it is saved in
the Database.
"""
super(MotionMixin, self).manipulate_object(form) super(MotionMixin, self).manipulate_object(form)
for attr in ['title', 'text', 'reason']: for attr in ['title', 'text', 'reason']:
setattr(self.object, attr, form.cleaned_data[attr]) setattr(self.object, attr, form.cleaned_data[attr])
try: if type(self) != MotionCreateView:
if form.cleaned_data['new_version']: if self.object.state.versioning and form.cleaned_data.get('new_version', True):
self.object.new_version self.object.new_version
except KeyError:
pass
try: try:
self.object.category = form.cleaned_data['category'] self.object.category = form.cleaned_data['category']
@ -210,29 +211,40 @@ motion_delete = MotionDeleteView.as_view()
class VersionPermitView(GetVersionMixin, SingleObjectMixin, QuestionMixin, RedirectView): class VersionPermitView(GetVersionMixin, SingleObjectMixin, QuestionMixin, RedirectView):
"""View to permit a version of a motion.""" """
View to permit a version of a motion.
"""
model = Motion model = Motion
question_url_name = 'motion_version_detail' question_url_name = 'motion_version_detail'
success_url_name = 'motion_version_detail' success_url_name = 'motion_version_detail'
success_message = ugettext_lazy('Version successfully permitted.')
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
"""Set self.object to a motion.""" """
Set self.object to a motion.
"""
self.object = self.get_object() self.object = self.get_object()
return super(VersionPermitView, self).get(*args, **kwargs) return super(VersionPermitView, self).get(*args, **kwargs)
def get_url_name_args(self): def get_url_name_args(self):
"""Return a list with arguments to create the success- and question_url.""" """
Return a list with arguments to create the success- and question_url.
"""
return [self.object.pk, self.object.version.version_number] return [self.object.pk, self.object.version.version_number]
def get_question(self): def get_question(self):
"""Return a string, shown to the user as question to permit the version.""" """
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 return _('Are you sure you want permit Version %s?') % self.object.version.version_number
def case_yes(self): def case_yes(self):
"""Activate the version, if the user chooses 'yes'.""" """
self.object.activate_version(self.object.version) # TODO: Write log message Activate the version, if the user chooses 'yes'.
self.object.save() """
self.object.set_active_version(self.object.version) # TODO: Write log message
self.object.save(no_new_version=True)
version_permit = VersionPermitView.as_view() version_permit = VersionPermitView.as_view()

View File

@ -137,6 +137,36 @@ class ModelTest(TestCase):
state_1.next_states.add(state_2) state_1.next_states.add(state_2)
state_1.save() state_1.save()
def test_two_empty_identifiers(self):
motion1 = Motion.objects.create(title='foo', text='bar', identifier='')
motion2 = Motion.objects.create(title='foo2', text='bar2', identifier='')
def test_do_not_create_new_version_when_permit_old_version(self):
motion = Motion()
motion.title = 'foo'
motion.text = 'bar'
first_version = motion.version
my_state = State.objects.create(name='automatic_versioning', workflow=self.workflow,
versioning=True, dont_set_new_version_active=True)
motion.state = my_state
motion.save()
motion = Motion.objects.get(pk=motion.pk)
motion.new_version
motion.title = 'New Title'
motion.save()
new_version = motion.last_version
self.assertEqual(motion.versions.count(), 2)
motion.set_active_version(new_version)
motion.save()
self.assertEqual(motion.versions.count(), 2)
motion.set_active_version(first_version)
motion.version = first_version
motion.save(no_new_version=True)
self.assertEqual(motion.versions.count(), 2)
class ConfigTest(TestCase): class ConfigTest(TestCase):
def test_stop_submitting(self): def test_stop_submitting(self):

View File

@ -165,3 +165,31 @@ class TestMotionDeleteView(MotionViewTestCase):
motion = Motion.objects.get(pk=2).add_submitter(self.delegate) motion = Motion.objects.get(pk=2).add_submitter(self.delegate)
response = self.delegate_client.post('/motion/2/del/', {}) response = self.delegate_client.post('/motion/2/del/', {})
self.assertRedirects(response, '/motion/') self.assertRedirects(response, '/motion/')
class TestVersionPermitView(MotionViewTestCase):
def setUp(self):
super(TestVersionPermitView, self).setUp()
self.motion1.new_version
self.motion1.save()
def test_get(self):
response = self.check_url('/motion/1/version/2/permit/', self.admin_client, 302)
self.assertRedirects(response, '/motion/1/version/2/')
def test_post(self):
new_version = self.motion1.last_version
response = self.admin_client.post('/motion/1/version/2/permit/', {'yes': 1})
self.assertRedirects(response, '/motion/1/version/2/')
self.assertEqual(self.motion1.active_version, new_version)
def test_activate_old_version(self):
new_version = self.motion1.last_version
first_version = self.motion1.versions.order_by('version_number')[0]
self.motion1.set_active_version(new_version)
self.assertEqual(self.motion1.versions.count(), 2)
response = self.admin_client.post('/motion/1/version/1/permit/', {'yes': 1})
self.motion1 = Motion.objects.get(pk=1)
self.assertEqual(self.motion1.active_version, first_version)
self.assertEqual(self.motion1.versions.count(), 2)