Merge pull request #638 from normanjaeckel/MotionRework

Motion identifier setting and versioning.
This commit is contained in:
Oskar Hahn 2013-05-13 14:43:56 -07:00
commit 7a4d40283d
7 changed files with 184 additions and 148 deletions

View File

@ -99,10 +99,27 @@ class MotionDisableVersioningMixin(forms.ModelForm):
last_version will be used."""
# TODO: Add category and identifier to the form as normal fields (the django way),
# not as 'new' field from 'new' forms.
class MotionCategoryMixin(forms.ModelForm):
"""Mixin to let the user choose the category for the motion."""
"""
Mixin to let the user choose the category for the motion.
"""
category = forms.ModelChoiceField(queryset=Category.objects.all(), required=False, label=ugettext_lazy("Category"))
"""
Category of the motion.
"""
def __init__(self, *args, **kwargs):
"""
Fill in the category of the motion as default value.
"""
if self.motion is not None:
category = self.motion.category
self.initial['category'] = category
super(MotionCategoryMixin, self).__init__(*args, **kwargs)
class MotionIdentifierMixin(forms.ModelForm):
@ -112,15 +129,9 @@ class MotionIdentifierMixin(forms.ModelForm):
identifier = forms.CharField(required=False, label=ugettext_lazy('Identifier'))
def clean_identifier(self):
"""
Test, that the identifier is unique
"""
identifier = self.cleaned_data['identifier']
if Motion.objects.filter(identifier=identifier).exists():
raise forms.ValidationError(_('The Identifier is not unique.'))
else:
return identifier
class Meta:
model = Motion
fields = ('identifier',)
class MotionImportForm(CssClassMixin, forms.Form):

View File

@ -102,63 +102,37 @@ class Motion(SlideMixin, models.Model):
return self.get_title()
# TODO: Use transaction
def save(self, no_new_version=False, *args, **kwargs):
def save(self, ignore_version_data=False, *args, **kwargs):
"""
Save the motion.
1. Set the state of a new motion to the default state.
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
'ALWAYS_CREATE_NEW_VERSION'.
If no_new_version is True, a new version will never be used.
2. Ensure that the identifier is not an empty string.
3. Save the motion object.
4. Save the version data, if ignore_version_data == False.
5. Set the active version for the motion, if ignore_version_data == False.
"""
if not self.state:
self.reset_state()
# TODO: Bad hack here to make Motion.objects.create() work
# again. We have to remove the flag to force an INSERT given
# by Django's create() method without knowing its advantages
# because of our misuse of the save() method in the
# set_identifier() method.
kwargs.pop('force_insert', None)
if not self.identifier and self.identifier is not None:
if not self.identifier and self.identifier is not None: # TODO: Why not >if self.identifier is '':<
self.identifier = None
super(Motion, self).save(*args, **kwargs)
if no_new_version:
return
# Find out if the version data has changed
for attr in ['title', 'text', 'reason']:
if not self.versions.exists():
new_data = True
break
if getattr(self, attr) != getattr(self.last_version, attr):
new_data = True
break
else:
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.
need_new_version = self.state.versioning
if hasattr(self, '_new_version') or (new_data and need_new_version):
if not ignore_version_data:
# Select version object
version = self.last_version
if hasattr(self, '_new_version'):
version = self.new_version
del self._new_version
version.motion = self # TODO: Test if this line is really neccessary.
elif new_data and not need_new_version:
version = self.last_version
else:
# We do not need to save the motion version.
return
# Save title, text and reason in the version object
for attr in ['title', 'text', 'reason']:
@ -182,7 +156,7 @@ class Motion(SlideMixin, models.Model):
# version is saved to the database
if self.active_version is None or not self.state.leave_old_version_active:
self.active_version = version
self.save()
self.save(ignore_version_data=True)
def get_absolute_url(self, link='detail'):
"""
@ -198,12 +172,16 @@ class Motion(SlideMixin, models.Model):
return reverse('motion_delete', args=[str(self.id)])
def set_identifier(self):
if config['motion_identifier'] == 'manually':
"""
Sets the motion identifier automaticly according to the config
value, if it is not set yet.
"""
if config['motion_identifier'] == 'manually' or self.identifier:
# Do not set an identifier.
return
elif config['motion_identifier'] == 'per_category':
motions = Motion.objects.filter(category=self.category)
else:
else: # That means: config['motion_identifier'] == 'serially_numbered'
motions = Motion.objects.all()
number = motions.aggregate(Max('identifier_number'))['identifier_number__max'] or 0
@ -212,23 +190,24 @@ class Motion(SlideMixin, models.Model):
else:
prefix = self.category.prefix + ' '
# TODO: Do not use the save() method in this method, see note in
# the save() method above.
while True:
number += 1
self.identifier = '%s%d' % (prefix, number)
self.identifier_number = number
try:
self.save()
self.save(ignore_version_data=True)
except IntegrityError:
continue
else:
self.number = number
self.save()
break
def get_title(self):
"""
Get the title of the motion.
The titel is taken from motion.version.
The title is taken from motion.version.
"""
try:
return self._title
@ -239,7 +218,7 @@ class Motion(SlideMixin, models.Model):
"""
Set the titel of the motion.
The titel will me saved into the version object, wenn motion.save() is
The title will be saved into the version object, wenn motion.save() is
called.
"""
self._title = title
@ -449,10 +428,11 @@ class Motion(SlideMixin, models.Model):
If the motion is new, it chooses the default workflow from config.
"""
if self.state:
self.state = self.state.workflow.first_state
new_state = self.state.workflow.first_state
else:
self.state = (Workflow.objects.get(pk=config['motion_workflow']).first_state or
new_state = (Workflow.objects.get(pk=config['motion_workflow']).first_state or
Workflow.objects.get(pk=config['motion_workflow']).state_set.all()[0])
self.set_state(new_state)
def slide(self):
"""

View File

@ -98,15 +98,15 @@ def setup_motion_config_page(sender, **kwargs):
choices=[(workflow.pk, ugettext_lazy(workflow.name)) for workflow in Workflow.objects.all()]))
motion_identifier = ConfigVariable(
name='motion_identifier',
default_value='manually',
default_value='serially_numbered',
form_field=forms.ChoiceField(
widget=forms.Select(),
required=False,
required=True,
label=ugettext_lazy('Identifier'),
choices=[
('manually', ugettext_lazy('Set it manually')),
('serially_numbered', ugettext_lazy('Serially numbered')),
('per_category', ugettext_lazy('Numbered per category')),
('serially_numbered', ugettext_lazy('Serially numbered'))]))
('manually', ugettext_lazy('Set it manually'))]))
return ConfigPage(title=ugettext_noop('Motion'),
url='motion',

View File

@ -40,11 +40,6 @@ urlpatterns = patterns('openslides.motion.views',
name='motion_delete',
),
url(r'^(?P<pk>\d+)/set_identifier/',
'set_identifier',
name='motion_set_identifier',
),
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/$',
'motion_detail',
name='motion_version_detail',

View File

@ -99,8 +99,16 @@ class MotionMixin(object):
for attr in ['title', 'text', 'reason']:
setattr(self.object, attr, form.cleaned_data[attr])
if type(self) != MotionCreateView:
if self.object.state.versioning and form.cleaned_data.get('new_version', True):
if type(self) == MotionCreateView:
self.object.new_version
else:
for attr in ['title', 'text', 'reason']:
if getattr(self.object, attr) != getattr(self.object.last_version, attr):
new_data = True
break
else:
new_data = False
if new_data and self.object.state.versioning and not form.cleaned_data.get('disable_versioning', False):
self.object.new_version
try:
@ -114,7 +122,9 @@ class MotionMixin(object):
pass
def post_save(self, form):
"""Save the submitter an the supporter so the motion."""
"""
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:
@ -129,7 +139,8 @@ class MotionMixin(object):
for person in form.cleaned_data['supporter']])
def get_form_class(self):
"""Return the FormClass to Create or Update the Motion.
"""
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
@ -138,7 +149,7 @@ class MotionMixin(object):
form_classes = []
if (self.request.user.has_perm('motion.can_manage_motion') and
config['motion_identifier'] == 'manually'):
(config['motion_identifier'] == 'manually' or type(self) == MotionUpdateView)):
form_classes.append(MotionIdentifierMixin)
form_classes.append(BaseMotionForm)
@ -247,33 +258,42 @@ class VersionPermitView(GetVersionMixin, SingleObjectMixin, QuestionMixin, Redir
Activate the version, if the user chooses 'yes'.
"""
self.object.set_active_version(self.object.version) # TODO: Write log message
self.object.save(no_new_version=True)
self.object.save(ignore_version_data=True)
version_permit = VersionPermitView.as_view()
class VersionRejectView(GetVersionMixin, SingleObjectMixin, QuestionMixin, RedirectView):
"""View to reject a version."""
"""
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."""
"""
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 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'."""
"""
Reject the version, if the user chooses 'yes'.
"""
self.object.reject_version(self.object.version) # TODO: Write log message
self.object.save()
self.object.save(ignore_version_data=True)
version_reject = VersionRejectView.as_view()
@ -311,30 +331,6 @@ class VersionDiffView(DetailView):
version_diff = VersionDiffView.as_view()
class SetIdentifierView(SingleObjectMixin, RedirectView):
"""Set the identifier of the motion.
See motion.set_identifier for more informations
"""
permission_required = 'motion.can_manage_motion'
model = Motion
url_name = 'motion_detail'
def get(self, request, *args, **kwargs):
"""Set self.object to a motion."""
self.object = self.get_object()
return super(SetIdentifierView, self).get(request, *args, **kwargs)
def pre_redirect(self, request, *args, **kwargs):
"""Set the identifier."""
self.object.set_identifier()
def get_url_name_args(self):
return [self.object.id]
set_identifier = SetIdentifierView.as_view()
class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
"""View to support or unsupport a motion.
@ -544,10 +540,10 @@ poll_pdf = PollPDFView.as_view()
class MotionSetStateView(SingleObjectMixin, RedirectView):
"""View to set the state of a motion.
"""
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'
@ -556,7 +552,9 @@ class MotionSetStateView(SingleObjectMixin, RedirectView):
reset = False
def pre_redirect(self, request, *args, **kwargs):
"""Save the new state and write a log message."""
"""
Save the new state and write a log message.
"""
self.object = self.get_object()
try:
if self.reset:
@ -566,17 +564,13 @@ class MotionSetStateView(SingleObjectMixin, RedirectView):
except WorkflowError, e: # TODO: Is a WorkflowError still possible here?
messages.error(request, e)
else:
self.object.save()
self.object.save(ignore_version_data=True)
# TODO: the state is not translated
self.object.write_log(ugettext_noop('State changed to %s') %
self.object.state.name, self.request.user)
messages.success(request, _('Motion status was set to: %s.'
% 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()
reset_state = MotionSetStateView.as_view(reset=True)

View File

@ -36,8 +36,8 @@ class ModelTest(TestCase):
motion.save()
self.assertEqual(motion.versions.count(), 2)
motion.state = State.objects.create(name='automatic_versioning', workflow=self.workflow, versioning=True)
motion.text = 'new text'
motion.new_version
motion.save()
self.assertEqual(motion.versions.count(), 3)
@ -58,11 +58,13 @@ class ModelTest(TestCase):
def test_version(self):
motion = Motion.objects.create(title='v1')
motion.state = State.objects.create(name='automatic_versioning', workflow=self.workflow, versioning=True)
motion.title = 'v2'
motion.new_version
motion.save()
v2_version = motion.version
motion.title = 'v3'
motion.new_version
motion.save()
with self.assertRaises(AttributeError):
self._title
@ -146,9 +148,6 @@ class ModelTest(TestCase):
motion.title = 'foo'
motion.text = 'bar'
first_version = motion.version
my_state = State.objects.create(name='automatic_versioning', workflow=self.workflow,
versioning=True, leave_old_version_active=True)
motion.state = my_state
motion.save()
motion = Motion.objects.get(pk=motion.pk)
@ -164,7 +163,7 @@ class ModelTest(TestCase):
motion.set_active_version(first_version)
motion.version = first_version
motion.save(no_new_version=True)
motion.save(ignore_version_data=True)
self.assertEqual(motion.versions.count(), 2)

View File

@ -13,7 +13,7 @@ from django.test.client import Client
from openslides.config.api import config
from openslides.utils.test import TestCase
from openslides.participant.models import User, Group
from openslides.motion.models import Motion
from openslides.motion.models import Motion, State
class MotionViewTestCase(TestCase):
@ -120,12 +120,13 @@ class TestMotionCreateView(MotionViewTestCase):
self.assertContains(response, 'href="/motion/new/"', status_code=200)
def test_identifier_not_unique(self):
Motion.objects.create(identifier='foo')
response = self.admin_client.post(self.url, {'title': 'foo',
Motion.objects.create(title='Another motion 3', identifier='uufag5faoX0thahBi8Fo')
config['motion_identifier'] = 'manually'
response = self.admin_client.post(self.url, {'title': 'something',
'text': 'bar',
'submitter': self.admin,
'identifier': 'foo'})
self.assertFormError(response, 'form', 'identifier', 'The Identifier is not unique.')
'identifier': 'uufag5faoX0thahBi8Fo'})
self.assertFormError(response, 'form', 'identifier', 'Motion with this Identifier already exists.')
def test_empty_text_field(self):
response = self.admin_client.post(self.url, {'title': 'foo',
@ -162,6 +163,62 @@ class TestMotionUpdateView(MotionViewTestCase):
motion = Motion.objects.get(pk=1)
self.assertEqual(motion.title, 'my title')
def test_versioning(self):
self.assertFalse(self.motion1.state.versioning)
versioning_state = State.objects.create(name='automatic_versioning', workflow=self.motion1.state.workflow, versioning=True)
self.motion1.state = versioning_state
self.motion1.save()
motion = Motion.objects.get(pk=self.motion1.pk)
self.assertTrue(self.motion1.state.versioning)
self.assertEqual(motion.versions.count(), 1)
response = self.admin_client.post(self.url, {'title': 'another new motion_title',
'text': 'another motion text',
'reason': 'another motion reason',
'submitter': self.admin})
self.assertRedirects(response, '/motion/1/')
motion = Motion.objects.get(pk=self.motion1.pk)
self.assertEqual(motion.versions.count(), 2)
def test_disable_versioning(self):
self.assertFalse(self.motion1.state.versioning)
versioning_state = State.objects.create(name='automatic_versioning', workflow=self.motion1.state.workflow, versioning=True)
self.motion1.state = versioning_state
self.motion1.save()
motion = Motion.objects.get(pk=self.motion1.pk)
self.assertTrue(self.motion1.state.versioning)
config['motion_allow_disable_versioning'] = True
self.assertEqual(motion.versions.count(), 1)
response = self.admin_client.post(self.url, {'title': 'another new motion_title',
'text': 'another motion text',
'reason': 'another motion reason',
'submitter': self.admin,
'disable_versioning': 'true'})
self.assertRedirects(response, '/motion/1/')
motion = Motion.objects.get(pk=self.motion1.pk)
self.assertEqual(motion.versions.count(), 1)
def test_no_versioning_without_new_data(self):
self.assertFalse(self.motion1.state.versioning)
versioning_state = State.objects.create(name='automatic_versioning', workflow=self.motion1.state.workflow, versioning=True)
self.motion1.state = versioning_state
self.motion1.title = 'Chah4kaaKasiVuishi5x'
self.motion1.text = 'eedieFoothae2iethuo3'
self.motion1.reason = 'ier2laiy1veeGoo0mau2'
self.motion1.save()
motion = Motion.objects.get(pk=self.motion1.pk)
self.assertTrue(self.motion1.state.versioning)
self.assertEqual(motion.versions.count(), 1)
response = self.admin_client.post(self.url, {'title': 'Chah4kaaKasiVuishi5x',
'text': 'eedieFoothae2iethuo3',
'reason': 'ier2laiy1veeGoo0mau2',
'submitter': self.admin})
self.assertRedirects(response, '/motion/1/')
motion = Motion.objects.get(pk=self.motion1.pk)
self.assertEqual(motion.versions.count(), 1)
class TestMotionDeleteView(MotionViewTestCase):
def test_get(self):