Rework of the motion model, to make it more explcit.

This commit is contained in:
Oskar Hahn 2013-06-01 12:36:42 +02:00
parent d4739f5dd7
commit cd19920223
6 changed files with 329 additions and 299 deletions

View File

@ -67,7 +67,8 @@ class BaseMotionForm(CleanHtmlFormMixin, CssClassMixin, forms.ModelForm):
class MotionSubmitterMixin(forms.ModelForm): class MotionSubmitterMixin(forms.ModelForm):
"""Mixin to append the submitter field to a MotionForm.""" """Mixin to append the submitter field to a MotionForm."""
submitter = MultiplePersonFormField(label=ugettext_lazy("Submitter")) submitter = MultiplePersonFormField(label=ugettext_lazy("Submitter"),
required=False)
"""Submitter of the motion. Can be one or more persons.""" """Submitter of the motion. Can be one or more persons."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -93,8 +93,7 @@ class Motion(SlideMixin, models.Model):
('can_support_motion', ugettext_noop('Can support motions')), ('can_support_motion', ugettext_noop('Can support motions')),
('can_manage_motion', ugettext_noop('Can manage motions')), ('can_manage_motion', ugettext_noop('Can manage motions')),
) )
# TODO: order per default by category and identifier ordering = ('identifier', )
# ordering = ('number',)
def __unicode__(self): def __unicode__(self):
""" """
@ -103,67 +102,90 @@ class Motion(SlideMixin, models.Model):
return self.active_version.title return self.active_version.title
# TODO: Use transaction # TODO: Use transaction
def save(self, ignore_version_data=False, *args, **kwargs): def save(self, use_version=None, *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. Ensure that the identifier is not an empty string. 2. Ensure that the identifier is not an empty string.
3. Save the motion object. 3. Save the motion object.
4. Save the version data, if ignore_version_data == False. 4. Save the version data
5. Set the active version for the motion, if ignore_version_data == False. 5. Set the active version for the motion, if a new version object was saved.
The version data is *not* saved, if
1. The django-feature 'update_fields' is used or
2. The argument use_version is False (differ to None).
The version object into which the data is saved is picked in this order:
1. The argument use_version.
2. The attribute use_version. As default, use_version is the
active_version. If the active_version is not set, it is the
last_version. If the last_version is not set, it is a
new_version. See use_version property.
use_version is the version object, in which the version data is saved.
* If use_version is False, no version data ist saved.
* If use_version is None, the last version is used.
To use a new version object, you have to set it via use_version. You have
to set the title, text and reason into this version object. motion.title
etc will be ignored.
""" """
if not self.state: if not self.state:
self.reset_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: # TODO: Why not >if self.identifier is '':< # Solves the problem, that there can only be one motion with an empty
# string as identifier
if self.identifier is '':
self.identifier = None self.identifier = None
super(Motion, self).save(*args, **kwargs) super(Motion, self).save(*args, **kwargs)
if not ignore_version_data: if 'update_fields' in kwargs:
# Select version object # Do not save the version-data, if only some motion fields are updated
version = self.last_version return
if hasattr(self, '_new_version'):
version = self.new_version
del self._new_version
version.motion = self # TODO: Test if this line is really neccessary.
# Save title, text and reason in the version object if use_version is False:
# We do not need to save the version
return
elif use_version is None:
use_version = self.get_last_version()
# Save title, text and reason in the version object.
for attr in ['title', 'text', 'reason']: for attr in ['title', 'text', 'reason']:
_attr = '_%s' % attr _attr = '_%s' % attr
try: data = getattr(self, _attr, None)
setattr(version, attr, getattr(self, _attr)) if data is not None:
setattr(use_version, attr, data)
delattr(self, _attr) delattr(self, _attr)
except AttributeError:
if self.versions.exists():
# If the _attr was not set, use the value from last_version
setattr(version, attr, getattr(self.last_version, attr))
# Set version_number of the new Version (if neccessary) and save it into the DB # If version is not in the database, test if it has new data and set
if version.id is None: # the version_number
# TODO: auto increment the version_number in the Database if use_version.id is None:
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0 if not self.version_data_changed(use_version):
version.version_number = version_number + 1 # We do not need to save the version
version.save() return
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
use_version.version_number = version_number + 1
# Set the active version of this motion. This has to be done after the # Necessary line, if the version was set before the motion had an id.
# version is saved to the database # propably a django bug.
if self.active_version is None or not self.state.leave_old_version_active: use_version.motion = use_version.motion
self.active_version = version
self.save(ignore_version_data=True) use_version.save()
# 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:
self.active_version = use_version
self.save(update_fields=['active_version'])
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', 'update' or 'delete'. The keyword argument 'link' can be 'detail', 'view', 'edit',
'update' or 'delete'.
""" """
if link == 'view' or link == 'detail': if link == 'view' or link == 'detail':
return reverse('motion_detail', args=[str(self.id)]) return reverse('motion_detail', args=[str(self.id)])
@ -172,6 +194,23 @@ class Motion(SlideMixin, models.Model):
if link == 'delete': if link == 'delete':
return reverse('motion_delete', args=[str(self.id)]) return reverse('motion_delete', args=[str(self.id)])
def version_data_changed(self, version):
"""
Compare the version with the last version of the motion.
Returns True if the version data (title, text, reason) is different.
Else, returns False.
"""
if not self.versions.exists():
# if there is no version in the database, the data has always changed
return True
last_version = self.get_last_version()
for attr in ['title', 'text', 'reason']:
if getattr(last_version, attr) != getattr(version, attr):
return True
return False
def set_identifier(self): def set_identifier(self):
""" """
Sets the motion identifier automaticly according to the config Sets the motion identifier automaticly according to the config
@ -189,20 +228,16 @@ class Motion(SlideMixin, models.Model):
if self.category is None or not self.category.prefix: if self.category is None or not self.category.prefix:
prefix = '' prefix = ''
else: else:
prefix = self.category.prefix + ' ' prefix = '%s ' % self.category.prefix
# TODO: Do not use the save() method in this method, see note in number += 1
# the save() method above. identifier = '%s%d' % (prefix, number)
while True: while Motion.objects.filter(identifier=identifier).exists():
number += 1 number += 1
self.identifier = '%s%d' % (prefix, number) identifier = '%s%d' % (prefix, number)
self.identifier_number = number
try: self.identifier = identifier
self.save(ignore_version_data=True) self.identifier_number = number
except IntegrityError:
continue
else:
break
def get_title(self): def get_title(self):
""" """
@ -213,7 +248,7 @@ class Motion(SlideMixin, models.Model):
try: try:
return self._title return self._title
except AttributeError: except AttributeError:
return self.version.title return self.get_active_version().title
def set_title(self, title): def set_title(self, title):
""" """
@ -240,7 +275,7 @@ class Motion(SlideMixin, models.Model):
try: try:
return self._text return self._text
except AttributeError: except AttributeError:
return self.version.text return self.get_active_version().text
def set_text(self, text): def set_text(self, text):
""" """
@ -266,7 +301,7 @@ class Motion(SlideMixin, models.Model):
try: try:
return self._reason return self._reason
except AttributeError: except AttributeError:
return self.version.reason return self.get_active_version().reason
def set_reason(self, reason): def set_reason(self, reason):
""" """
@ -283,72 +318,47 @@ class Motion(SlideMixin, models.Model):
Is saved in a MotionVersion object. Is saved in a MotionVersion object.
""" """
@property def get_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 The version data of the new version object is populated with the data
use the existing new version. set via motion.title, motion.text, motion.reason. If the data is not set,
it is population with the data from the last version object.
The new_version object will be deleted when it is saved into the db.
""" """
try: new_version = MotionVersion(motion=self)
return self._new_version if self.versions.exists():
except AttributeError: last_version = self.get_last_version()
self._new_version = MotionVersion(motion=self)
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.
"""
try:
return self._version
except AttributeError:
return self.last_version
def set_version(self, version):
"""
Set the 'active' version object.
The keyword argument 'version' can be a MotionVersion object or the
version_number of a version object or None.
If the argument is None, the newest version will be used.
"""
if version is None:
try:
del self._version
except AttributeError:
pass
else: else:
if type(version) is int: last_version = None
version = self.versions.get(version_number=version) for attr in ['title', 'text', 'reason']:
elif type(version) is not MotionVersion: _attr = '_%s' % attr
raise ValueError('The argument \'version\' has to be int or ' data = getattr(self, _attr, None)
'MotionVersion, not %s' % type(version)) if data is None and not last_version is None:
# TODO: Test, that the version is one of this motion data = getattr(last_version, attr)
self._version = version if data is not None:
setattr(new_version, attr, data)
return new_version
version = property(get_version, set_version) def get_active_version(self):
""" """
The active version of this motion. Returns the active version of the motion.
"""
@property If no active version is set by now, the last_version is used
def last_version(self): """
if self.active_version:
return self.active_version
else:
return self.get_last_version()
def get_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.
# Check whether the case, that a motion has not any version, can still appear.
try: try:
return self.versions.order_by('-version_number')[0] return self.versions.order_by('-version_number')[0]
except IndexError: except IndexError:
return self.new_version return self.get_new_version()
@property @property
def submitters(self): def submitters(self):
@ -459,7 +469,7 @@ class Motion(SlideMixin, models.Model):
""" """
Return a title for the Agenda. Return a title for the Agenda.
""" """
return self.active_version.title return self.title
def get_agenda_title_supplement(self): def get_agenda_title_supplement(self):
""" """
@ -518,16 +528,6 @@ class Motion(SlideMixin, models.Model):
""" """
MotionLog.objects.create(motion=self, message_list=message_list, person=person) MotionLog.objects.create(motion=self, message_list=message_list, person=person)
def set_active_version(self, version):
"""
Set the active version of a motion to 'version'.
'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
class MotionVersion(models.Model): class MotionVersion(models.Model):
""" """

View File

@ -8,7 +8,7 @@
{% block content %} {% block content %}
<h1> <h1>
{{ motion.title }} {{ motion.category|default:'' }} {{ title }} {{ motion.category|default:'' }}
<br> <br>
<small> <small>
{% if motion.identifier %} {% if motion.identifier %}
@ -17,7 +17,7 @@
<i>[{% trans "no number" %}]</i>, <i>[{% trans "no number" %}]</i>,
{% endif %} {% endif %}
{# TODO: show only for workflow with versioning #} {# TODO: show only for workflow with versioning #}
{% trans "Version" %} {{ motion.version.version_number }} {% trans "Version" %} {{ version.version_number }}
</small> </small>
<small class="pull-right"> <small class="pull-right">
<a href="{% url 'motion_list' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans "Back to overview" %}</a> <a href="{% url 'motion_list' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans "Back to overview" %}</a>
@ -57,33 +57,31 @@
<div class="row-fluid"> <div class="row-fluid">
<div class="span8"> <div class="span8">
{# TODO: show only for workflow with versioning #} {# TODO: show only for workflow with versioning #}
{% if motion.version.version_number != motion.last_version.version_number %} {% with last_version=motion.get_last_version active_version=motion.get_active_version %}
<span class="label label-warning"> {% if version.version_number != last_version.version_number %}
<i class="icon-warning-sign icon-white"></i> {% trans "This is not the newest version." %} <span class="label label-warning">
</span> <i class="icon-warning-sign icon-white"></i> {% trans "This is not the newest version." %}
<a href="{% model_url motion.last_version %}" class="btn btn-small">{% trans "Go to the newest version" %} </span>
(# {{ motion.last_version.version_number }})</a> <a href="{% model_url last_version %}" class="btn btn-small">{% trans "Go to the newest version" %}
{% endif %} (# {{ last_version.version_number }})</a>
{% if motion.version.version_number != motion.active_version.version_number %} {% endif %}
<span class="label label-warning"> {% if version.version_number != active_version.version_number %}
<i class="icon-warning-sign icon-white"></i> {% trans "This version is not authorized." %} <span class="label label-warning">
</span> <i class="icon-warning-sign icon-white"></i> {% trans "This version is not authorized." %}
<a href="{% model_url motion.active_version %}" class="btn btn-small">{% trans "Go to the authorized version" %} </span>
(# {{ motion.active_version.version_number }})</a> <a href="{% model_url active_version %}" class="btn btn-small">{% trans "Go to the authorized version" %}
{% endif %} (# {{ active_version.version_number }})</a>
{% endif %}
{% endwith %}
<!-- Text --> <!-- Text -->
<h4>{% trans "Motion text" %}:</h4> <h4>{% trans "Motion text" %}:</h4>
{{ motion.version.text|safe }} {{ text|safe }}
<br> <br>
<!-- Reason --> <!-- Reason -->
<h4>{% trans "Reason" %}:</h4> <h4>{% trans "Reason" %}:</h4>
{% if motion.version.reason %} {{ reason|safe|default:'' }}
{{ motion.version.reason|safe }}
{% else %}
{% endif %}
<br> <br>
<!-- Version history --> <!-- Version history -->
@ -102,7 +100,7 @@
<th>{% trans "Actions" %}</th> <th>{% trans "Actions" %}</th>
</tr> </tr>
{% endif %} {% endif %}
<tr {% if version == motion.version %}class="offline"{%endif %}> <tr {% if version == motion.use_version %}class="offline"{%endif %}>
<td class="nobr"> <td class="nobr">
{% if version == motion.active_version %} {% if version == motion.active_version %}
<span class="badge badge-success" title="{% trans 'This version is authorized' %}"><i class="icon-ok icon-white"></i></span> <span class="badge badge-success" title="{% trans 'This version is authorized' %}"><i class="icon-ok icon-white"></i></span>
@ -239,7 +237,7 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</h5> </h5>
{{ motion.version.creation_time }} {{ motion.use_version.creation_time }}
<!-- Widthdraw button --> <!-- Widthdraw button -->
{# TODO: Check this button #} {# TODO: Check this button #}

View File

@ -18,7 +18,7 @@ from django.db import transaction
from django.db.models import Model from django.db.models import Model
from django.utils.translation import ugettext as _, ugettext_lazy, ugettext_noop from django.utils.translation import ugettext as _, ugettext_lazy, ugettext_noop
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.http import Http404 from django.http import Http404, HttpResponseRedirect
from reportlab.platypus import SimpleDocTemplate from reportlab.platypus import SimpleDocTemplate
@ -44,74 +44,71 @@ from .csv_import import import_motions
class MotionListView(ListView): class MotionListView(ListView):
"""View, to list all motions.""" """
View, to list all motions.
"""
permission_required = 'motion.can_see_motion' permission_required = 'motion.can_see_motion'
model = Motion model = Motion
motion_list = MotionListView.as_view() motion_list = MotionListView.as_view()
class GetVersionMixin(object): class MotionDetailView(DetailView):
"""Mixin to set a specific version to a motion.""" """
Show one 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:
try:
object.version = int(version_number)
except MotionVersion.DoesNotExist:
raise Http404('Version %s not found' % version_number)
else:
object.version = object.active_version
return object
class MotionDetailView(GetVersionMixin, DetailView):
"""Show one motion."""
permission_required = 'motion.can_see_motion' permission_required = 'motion.can_see_motion'
model = Motion model = Motion
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Return the template context. """
Return the template context.
Append the allowed actions for the motion to the context. Append the allowed actions for the motion to the context.
""" """
context = super(MotionDetailView, self).get_context_data(**kwargs) version_number = self.kwargs.get('version_number', None)
context['allowed_actions'] = self.object.get_allowed_actions(self.request.user) if version_number is not None:
return context try:
version = self.object.versions.get(version_number=int(version_number))
except MotionVersion.DoesNotExist:
raise Http404('Version %s not found' % version_number)
else:
version = self.object.get_active_version()
kwargs.update({
'allowed_actions': self.object.get_allowed_actions(self.request.user),
'version': version,
'title': version.title,
'text': version.text,
'reason': version.reason})
return super(MotionDetailView, self).get_context_data(**kwargs)
motion_detail = MotionDetailView.as_view() motion_detail = MotionDetailView.as_view()
class MotionMixin(object): class MotionEditMixin(object):
""" """
Mixin for MotionViewsClasses to save the version data. Mixin for MotionViewsClasses to save the version data.
""" """
def manipulate_object(self, form): def form_valid(self, form):
""" """
Saves the version data into the motion object before it is saved in Saves the Create or UpdateForm into a motion object.
the Database. Does also set category, identifier and new workflow
if given.
""" """
super(MotionMixin, self).manipulate_object(form) self.object = form.save(commit=False)
for attr in ['title', 'text', 'reason']:
setattr(self.object, attr, form.cleaned_data[attr])
if type(self) == MotionCreateView: if type(self) == MotionUpdateView:
self.object.new_version # Decide if a new version is saved to the database
else: if (self.object.state.versioning and
for attr in ['title', 'text', 'reason']: not form.cleaned_data.get('disable_versioning', False)):
if getattr(self.object, attr) != getattr(self.object.last_version, attr): version = self.object.get_new_version()
new_data = True
break
else: else:
new_data = False version = self.object.get_last_version()
if new_data and self.object.state.versioning and not form.cleaned_data.get('disable_versioning', False): else:
self.object.new_version version = self.object.get_new_version()
for attr in ['title', 'text', 'reason']:
setattr(version, attr, form.cleaned_data[attr])
try: try:
self.object.category = form.cleaned_data['category'] self.object.category = form.cleaned_data['category']
@ -127,11 +124,9 @@ class MotionMixin(object):
if workflow: if workflow:
self.object.reset_state(workflow) self.object.reset_state(workflow)
def post_save(self, form): self.object.save(use_version=version)
"""
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 # TODO: only delete and save neccessary submitters and supporter
if 'submitter' in form.cleaned_data: if 'submitter' in form.cleaned_data:
self.object.submitter.all().delete() self.object.submitter.all().delete()
@ -143,6 +138,7 @@ class MotionMixin(object):
MotionSupporter.objects.bulk_create( MotionSupporter.objects.bulk_create(
[MotionSupporter(motion=self.object, person=person) [MotionSupporter(motion=self.object, person=person)
for person in form.cleaned_data['supporter']]) for person in form.cleaned_data['supporter']])
return HttpResponseRedirect(self.get_success_url())
def get_form_class(self): def get_form_class(self):
""" """
@ -173,8 +169,10 @@ class MotionMixin(object):
return type('MotionForm', tuple(form_classes), {}) return type('MotionForm', tuple(form_classes), {})
class MotionCreateView(MotionMixin, CreateView): class MotionCreateView(MotionEditMixin, CreateView):
"""View to create a motion.""" """
View to create a motion.
"""
model = Motion model = Motion
def has_permission(self, request, *args, **kwargs): def has_permission(self, request, *args, **kwargs):
@ -190,21 +188,22 @@ class MotionCreateView(MotionMixin, CreateView):
return False return False
def form_valid(self, form): def form_valid(self, form):
"""Write a log message, if the form is valid.""" """
value = super(MotionCreateView, self).form_valid(form) Write a log message, if the form is valid.
"""
response = super(MotionCreateView, self).form_valid(form)
self.object.write_log([ugettext_noop('Motion created')], self.request.user) self.object.write_log([ugettext_noop('Motion created')], self.request.user)
return value
def post_save(self, form):
super(MotionCreateView, self).post_save(form)
if not 'submitter' in form.cleaned_data: if not 'submitter' in form.cleaned_data:
self.object.add_submitter(self.request.user) self.object.add_submitter(self.request.user)
return response
motion_create = MotionCreateView.as_view() motion_create = MotionCreateView.as_view()
class MotionUpdateView(MotionMixin, UpdateView): class MotionUpdateView(MotionEditMixin, UpdateView):
"""View to update a motion.""" """
View to update a motion.
"""
model = Motion model = Motion
def has_permission(self, request, *args, **kwargs): def has_permission(self, request, *args, **kwargs):
@ -213,32 +212,28 @@ class MotionUpdateView(MotionMixin, UpdateView):
def form_valid(self, form): def form_valid(self, form):
"""Write a log message, if the form is valid.""" """Write a log message, if the form is valid."""
value = super(MotionUpdateView, self).form_valid(form) response = super(MotionUpdateView, self).form_valid(form)
self.object.write_log([ugettext_noop('Motion updated')], self.request.user)
return value
def manipulate_object(self, *args, **kwargs):
"""
Removes the supporters if config option is True and supporting is still
available in the state.
"""
return_value = super(MotionUpdateView, self).manipulate_object(*args, **kwargs)
if (config['motion_remove_supporters'] and self.object.state.allow_support and if (config['motion_remove_supporters'] and self.object.state.allow_support and
not self.request.user.has_perm('motion.can_manage_motion')): not self.request.user.has_perm('motion.can_manage_motion')):
self.object.clear_supporters() self.object.clear_supporters()
self.object.write_log([ugettext_noop('All supporters removed')], self.request.user) self.object.write_log([ugettext_noop('All supporters removed')], self.request.user)
return return_value self.object.write_log([ugettext_noop('Motion updated')], self.request.user)
return response
motion_edit = MotionUpdateView.as_view() motion_edit = MotionUpdateView.as_view()
class MotionDeleteView(DeleteView): class MotionDeleteView(DeleteView):
"""View to delete a motion.""" """
View to delete a motion.
"""
model = Motion model = Motion
success_url_name = 'motion_list' success_url_name = 'motion_list'
def has_permission(self, request, *args, **kwargs): def has_permission(self, request, *args, **kwargs):
"""Check if the request.user has the permission to delete the motion.""" """
Check if the request.user has the permission to delete the motion.
"""
return self.get_object().get_allowed_actions(request.user)['delete'] return self.get_object().get_allowed_actions(request.user)['delete']
def get_success_message(self): def get_success_message(self):
@ -247,7 +242,7 @@ class MotionDeleteView(DeleteView):
motion_delete = MotionDeleteView.as_view() motion_delete = MotionDeleteView.as_view()
class VersionPermitView(GetVersionMixin, SingleObjectMixin, QuestionMixin, RedirectView): class VersionPermitView(SingleObjectMixin, QuestionMixin, RedirectView):
""" """
View to permit a version of a motion. View to permit a version of a motion.
""" """
@ -262,46 +257,56 @@ class VersionPermitView(GetVersionMixin, SingleObjectMixin, QuestionMixin, Redir
Set self.object to a motion. Set self.object to a motion.
""" """
self.object = self.get_object() self.object = self.get_object()
version_number = self.kwargs.get('version_number', None)
try:
self.version = self.object.versions.get(version_number=int(version_number))
except MotionVersion.DoesNotExist:
raise Http404('Version %s not found' % version_number)
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. Returns 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.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.version.version_number
def case_yes(self): def case_yes(self):
""" """
Activate the version, if the user chooses 'yes'. Activate the version, if the user chooses 'yes'.
""" """
self.object.set_active_version(self.object.version) self.object.active_version = self.version
self.object.save(ignore_version_data=True) self.object.save(update_fields=['active_version'])
self.object.write_log( self.object.write_log(
message_list=[ugettext_noop('Version %d permitted') % self.object.version.version_number], message_list=[ugettext_noop('Version %d permitted')
% self.version.version_number],
person=self.request.user) person=self.request.user)
version_permit = VersionPermitView.as_view() version_permit = VersionPermitView.as_view()
class VersionDiffView(DetailView): class VersionDiffView(DetailView):
"""Show diff between two versions of a motion.""" """
Show diff between two versions of a motion.
"""
permission_required = 'motion.can_see_motion' permission_required = 'motion.can_see_motion'
model = Motion model = Motion
template_name = 'motion/motion_diff.html' template_name = 'motion/motion_diff.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Return the template context with versions and html diff strings.""" """
Return the template context with versions and html diff strings.
"""
try: try:
rev1 = int(self.request.GET['rev1']) rev1 = int(self.request.GET['rev1'])
rev2 = int(self.request.GET['rev2']) rev2 = int(self.request.GET['rev2'])
version_rev1 = self.object.versions.get(version_number=self.request.GET['rev1']) version_rev1 = self.object.versions.get(version_number=rev1)
version_rev2 = self.object.versions.get(version_number=self.request.GET['rev2']) version_rev2 = self.object.versions.get(version_number=rev2)
diff_text = htmldiff(version_rev1.text, version_rev2.text) diff_text = htmldiff(version_rev1.text, version_rev2.text)
diff_reason = htmldiff(version_rev1.reason, version_rev2.reason) diff_reason = htmldiff(version_rev1.reason, version_rev2.reason)
except (KeyError, ValueError, MotionVersion.DoesNotExist): except (KeyError, ValueError, MotionVersion.DoesNotExist):
@ -323,7 +328,8 @@ version_diff = VersionDiffView.as_view()
class SupportView(SingleObjectMixin, QuestionMixin, RedirectView): class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
"""View to support or unsupport a motion. """
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 True, the view will append a request.user to the supporter list.
@ -335,12 +341,16 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
support = True support = True
def get(self, request, *args, **kwargs): def get(self, request, *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(SupportView, self).get(request, *args, **kwargs) return super(SupportView, self).get(request, *args, **kwargs)
def check_permission(self, request): def check_permission(self, request):
"""Return True if the user can support or unsupport the motion. Else: False.""" """
Return True if the user can support or unsupport the motion. Else: False.
"""
allowed_actions = self.object.get_allowed_actions(request.user) allowed_actions = self.object.get_allowed_actions(request.user)
if self.support and not allowed_actions['support']: if self.support and not allowed_actions['support']:
messages.error(request, _('You can not support this motion.')) messages.error(request, _('You can not support this motion.'))
@ -352,14 +362,17 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
return True return True
def get_question(self): def get_question(self):
"""Return the question string.""" """
Return the question string.
"""
if self.support: if self.support:
return _('Do you really want to support this motion?') return _('Do you really want to support this motion?')
else: else:
return _('Do you really want to unsupport this motion?') return _('Do you really want to unsupport this motion?')
def case_yes(self): def case_yes(self):
"""Append or remove the request.user from the motion. """
Append or remove the request.user from the motion.
First the method checks the permissions, and writes a log message after First the method checks the permissions, and writes a log message after
appending or removing the user. appending or removing the user.
@ -374,14 +387,18 @@ class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
self.object.write_log([ugettext_noop("Supporter: -%s") % user], user) self.object.write_log([ugettext_noop("Supporter: -%s") % user], user)
def get_success_message(self): def get_success_message(self):
"""Return the success message.""" """
Return the success message.
"""
if self.support: if self.support:
return _("You have supported this motion successfully.") return _("You have supported this motion successfully.")
else: else:
return _("You have unsupported this motion successfully.") return _("You have unsupported this motion successfully.")
def get_redirect_url(self, **kwargs): def get_redirect_url(self, **kwargs):
"""Return the url, the view should redirect to.""" """
Return the url, the view should redirect to.
"""
return self.object.get_absolute_url() return self.object.get_absolute_url()
motion_support = SupportView.as_view(support=True) motion_support = SupportView.as_view(support=True)
@ -389,23 +406,31 @@ motion_unsupport = SupportView.as_view(support=False)
class PollCreateView(SingleObjectMixin, RedirectView): class PollCreateView(SingleObjectMixin, RedirectView):
"""View to create a poll for a motion.""" """
View to create a poll for a motion.
"""
permission_required = 'motion.can_manage_motion' permission_required = 'motion.can_manage_motion'
model = Motion model = Motion
def get(self, request, *args, **kwargs): def get(self, request, *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(PollCreateView, self).get(request, *args, **kwargs) return super(PollCreateView, self).get(request, *args, **kwargs)
def pre_redirect(self, request, *args, **kwargs): def pre_redirect(self, request, *args, **kwargs):
"""Create the poll for the motion.""" """
Create the poll for the motion.
"""
self.poll = self.object.create_poll() self.poll = self.object.create_poll()
self.object.write_log([ugettext_noop("Poll created")], request.user) self.object.write_log([ugettext_noop("Poll created")], request.user)
messages.success(request, _("New vote was successfully created.")) messages.success(request, _("New vote was successfully created."))
def get_redirect_url(self, **kwargs): def get_redirect_url(self, **kwargs):
"""Return the URL to the EditView of the poll.""" """
Return the URL to the EditView of the poll.
"""
return reverse('motion_poll_edit', args=[self.object.pk, self.poll.poll_number]) return reverse('motion_poll_edit', args=[self.object.pk, self.poll.poll_number])
poll_create = PollCreateView.as_view() poll_create = PollCreateView.as_view()
@ -555,31 +580,38 @@ class MotionSetStateView(SingleObjectMixin, RedirectView):
except WorkflowError, e: # TODO: Is a WorkflowError still possible here? except WorkflowError, e: # TODO: Is a WorkflowError still possible here?
messages.error(request, e) messages.error(request, e)
else: else:
self.object.save(ignore_version_data=True) self.object.save(update_fields=['state'])
self.object.write_log( self.object.write_log(
message_list=[ugettext_noop('State changed to '), self.object.state.name], message_list=[ugettext_noop('State changed to '), self.object.state.name],
person=self.request.user) person=self.request.user)
messages.success(request, messages.success(request,
_('The state of the motion was set to %s.') % html_strong(_(self.object.state.name))) _('The state of the motion was set to %s.')
% html_strong(_(self.object.state.name)))
set_state = MotionSetStateView.as_view() set_state = MotionSetStateView.as_view()
reset_state = MotionSetStateView.as_view(reset=True) reset_state = MotionSetStateView.as_view(reset=True)
class CreateAgendaItemView(SingleObjectMixin, RedirectView): class CreateAgendaItemView(SingleObjectMixin, RedirectView):
"""View to create and agenda item for a motion.""" """
View to create and agenda item for a motion.
"""
permission_required = 'agenda.can_manage_agenda' permission_required = 'agenda.can_manage_agenda'
model = Motion model = Motion
url_name = 'item_overview' url_name = 'item_overview'
url_name_args = [] url_name_args = []
def get(self, request, *args, **kwargs): def get(self, request, *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(CreateAgendaItemView, self).get(request, *args, **kwargs) return super(CreateAgendaItemView, self).get(request, *args, **kwargs)
def pre_redirect(self, request, *args, **kwargs): def pre_redirect(self, request, *args, **kwargs):
"""Create the agenda item.""" """
Create the agenda item.
"""
self.item = Item.objects.create(related_sid=self.object.sid) self.item = Item.objects.create(related_sid=self.object.sid)
self.object.write_log([ugettext_noop('Agenda item created')], self.request.user) self.object.write_log([ugettext_noop('Agenda item created')], self.request.user)
@ -587,32 +619,40 @@ create_agenda_item = CreateAgendaItemView.as_view()
class MotionPDFView(SingleObjectMixin, PDFView): class MotionPDFView(SingleObjectMixin, PDFView):
"""Create the PDF for one, or all motions. """
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 True, the view returns a PDF with all motions.
If self.print_all_motions is False, the view returns a PDF with only one If self.print_all_motions is False, the view returns a PDF with only one
motion.""" motion.
"""
permission_required = 'motion.can_see_motion' permission_required = 'motion.can_see_motion'
model = Motion model = Motion
top_space = 0 top_space = 0
print_all_motions = False print_all_motions = False
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Set self.object to a motion.""" """
Set self.object to a motion.
"""
if not self.print_all_motions: if not self.print_all_motions:
self.object = self.get_object() self.object = self.get_object()
return super(MotionPDFView, self).get(request, *args, **kwargs) return super(MotionPDFView, self).get(request, *args, **kwargs)
def get_filename(self): def get_filename(self):
"""Return the filename for the PDF.""" """
Return the filename for the PDF.
"""
if self.print_all_motions: if self.print_all_motions:
return _("Motions") return _("Motions")
else: else:
return _("Motion: %s") % unicode(self.object) return _("Motion: %s") % unicode(self.object)
def append_to_pdf(self, pdf): def append_to_pdf(self, pdf):
"""Append PDF objects.""" """
Append PDF objects.
"""
if self.print_all_motions: if self.print_all_motions:
motions_to_pdf(pdf) motions_to_pdf(pdf)
else: else:
@ -694,7 +734,9 @@ motion_csv_import = MotionCSVImportView.as_view()
def register_tab(request): def register_tab(request):
"""Return the motion tab.""" """
Return the motion tab.
"""
# TODO: Find a better way to set the selected var. # TODO: Find a better way to set the selected var.
return Tab( return Tab(
title=_('Motions'), title=_('Motions'),
@ -705,7 +747,8 @@ def register_tab(request):
def get_widgets(request): def get_widgets(request):
"""Return the motion widgets for the dashboard. """
Return the motion widgets for the dashboard.
There is only one widget. It shows all motions. There is only one widget. It shows all motions.
""" """

View File

@ -19,30 +19,24 @@ class ModelTest(TestCase):
def setUp(self): def setUp(self):
self.motion = Motion.objects.create(title='v1') self.motion = Motion.objects.create(title='v1')
self.test_user = User.objects.create(username='blub') self.test_user = User.objects.create(username='blub')
# Use the simple workflow
self.workflow = Workflow.objects.get(pk=1) self.workflow = Workflow.objects.get(pk=1)
def test_create_new_version(self): def test_create_new_version(self):
motion = Motion.objects.create(title='m1') motion = self.motion
self.assertEqual(motion.versions.count(), 1) self.assertEqual(motion.versions.count(), 1)
motion.new_version # new data, but no new version
motion.save()
self.assertEqual(motion.versions.count(), 2)
motion.title = 'new title' motion.title = 'new title'
motion.save() motion.save()
self.assertEqual(motion.versions.count(), 2) self.assertEqual(motion.versions.count(), 1)
motion.save()
self.assertEqual(motion.versions.count(), 2)
# new data and new version
motion.text = 'new text' motion.text = 'new text'
motion.new_version motion.save(use_version=motion.get_new_version())
motion.save() self.assertEqual(motion.versions.count(), 2)
self.assertEqual(motion.versions.count(), 3) self.assertEqual(motion.title, 'new title')
self.assertEqual(motion.text, 'new text')
motion.save()
self.assertEqual(motion.versions.count(), 3)
def test_version_data(self): def test_version_data(self):
motion = Motion() motion = Motion()
@ -53,36 +47,24 @@ class ModelTest(TestCase):
motion.title = 'title' motion.title = 'title'
self.assertEqual(motion._title, 'title') self.assertEqual(motion._title, 'title')
motion.text = 'text'
self.assertEqual(motion._text, 'text')
motion.reason = 'reason' motion.reason = 'reason'
self.assertEqual(motion._reason, 'reason') self.assertEqual(motion._reason, 'reason')
def test_version(self): def test_version(self):
motion = Motion.objects.create(title='v1') motion = self.motion
motion.title = 'v2' motion.title = 'v2'
motion.new_version motion.save(use_version=motion.get_new_version())
motion.save() v2_version = motion.get_last_version()
v2_version = motion.version
motion.title = 'v3' motion.title = 'v3'
motion.new_version motion.save(use_version=motion.get_new_version())
motion.save()
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
self._title self._title
self.assertEqual(motion.title, 'v3') self.assertEqual(motion.title, 'v3')
motion.version = 1
self.assertEqual(motion.title, 'v1')
motion.version = v2_version
self.assertEqual(motion.title, 'v2')
motion.version = None
motion.version = None # Test to set a version to None, which is already None
self.assertEqual(motion.title, 'v3')
with self.assertRaises(ValueError):
motion.version = 'wrong'
def test_absolute_url(self): def test_absolute_url(self):
motion_id = self.motion.id motion_id = self.motion.id
@ -147,23 +129,21 @@ class ModelTest(TestCase):
motion = Motion() motion = Motion()
motion.title = 'foo' motion.title = 'foo'
motion.text = 'bar' motion.text = 'bar'
first_version = motion.version
motion.save() motion.save()
first_version = motion.get_last_version()
motion = Motion.objects.get(pk=motion.pk) motion = Motion.objects.get(pk=motion.pk)
motion.new_version
motion.title = 'New Title' motion.title = 'New Title'
motion.save() motion.save(use_version=motion.get_new_version())
new_version = motion.last_version new_version = motion.get_last_version()
self.assertEqual(motion.versions.count(), 2) self.assertEqual(motion.versions.count(), 2)
motion.set_active_version(new_version) motion.active_version = new_version
motion.save() motion.save()
self.assertEqual(motion.versions.count(), 2) self.assertEqual(motion.versions.count(), 2)
motion.set_active_version(first_version) motion.active_version = first_version
motion.version = first_version motion.save(use_version=False)
motion.save(ignore_version_data=True)
self.assertEqual(motion.versions.count(), 2) self.assertEqual(motion.versions.count(), 2)

View File

@ -65,6 +65,16 @@ class TestMotionDetailView(MotionViewTestCase):
self.check_url('/motion/500/', self.admin_client, 404) self.check_url('/motion/500/', self.admin_client, 404)
class TestMotionDetailVersionView(MotionViewTestCase):
def test_get(self):
self.motion1.title = 'AFWEROBjwerGwer'
self.motion1.save(use_version=self.motion1.get_new_version())
self.check_url('/motion/1/version/1/', self.admin_client, 200)
response = self.check_url('/motion/1/version/2/', self.admin_client, 200)
self.assertContains(response, 'AFWEROBjwerGwer')
self.check_url('/motion/1/version/500/', self.admin_client, 404)
class TestMotionCreateView(MotionViewTestCase): class TestMotionCreateView(MotionViewTestCase):
url = '/motion/new/' url = '/motion/new/'
@ -72,7 +82,6 @@ class TestMotionCreateView(MotionViewTestCase):
self.check_url(self.url, self.admin_client, 200) self.check_url(self.url, self.admin_client, 200)
def test_admin(self): def test_admin(self):
self.assertFalse(Motion.objects.filter(versions__title='new motion').exists())
response = self.admin_client.post(self.url, {'title': 'new motion', response = self.admin_client.post(self.url, {'title': 'new motion',
'text': 'motion text', 'text': 'motion text',
'reason': 'motion reason', 'reason': 'motion reason',
@ -81,7 +90,6 @@ class TestMotionCreateView(MotionViewTestCase):
self.assertTrue(Motion.objects.filter(versions__title='new motion').exists()) self.assertTrue(Motion.objects.filter(versions__title='new motion').exists())
def test_delegate(self): def test_delegate(self):
self.assertFalse(Motion.objects.filter(versions__title='delegate motion').exists())
response = self.delegate_client.post(self.url, {'title': 'delegate motion', response = self.delegate_client.post(self.url, {'title': 'delegate motion',
'text': 'motion text', 'text': 'motion text',
'reason': 'motion reason', 'reason': 'motion reason',
@ -91,7 +99,6 @@ class TestMotionCreateView(MotionViewTestCase):
self.assertTrue(motion.is_submitter(self.delegate)) self.assertTrue(motion.is_submitter(self.delegate))
def test_registered(self): def test_registered(self):
self.assertFalse(Motion.objects.filter(versions__title='registered motion').exists())
response = self.registered_client.post(self.url, {'title': 'registered motion', response = self.registered_client.post(self.url, {'title': 'registered motion',
'text': 'motion text', 'text': 'motion text',
'reason': 'motion reason', 'reason': 'motion reason',
@ -297,24 +304,25 @@ class TestMotionDeleteView(MotionViewTestCase):
class TestVersionPermitView(MotionViewTestCase): class TestVersionPermitView(MotionViewTestCase):
def setUp(self): def setUp(self):
super(TestVersionPermitView, self).setUp() super(TestVersionPermitView, self).setUp()
self.motion1.new_version self.motion1.title = 'new'
self.motion1.save() self.motion1.save(use_version=self.motion1.get_new_version())
def test_get(self): def test_get(self):
response = self.check_url('/motion/1/version/2/permit/', self.admin_client, 302) response = self.check_url('/motion/1/version/2/permit/', self.admin_client, 302)
self.assertRedirects(response, '/motion/1/version/2/') self.assertRedirects(response, '/motion/1/version/2/')
def test_post(self): def test_post(self):
new_version = self.motion1.last_version new_version = self.motion1.get_last_version()
response = self.admin_client.post('/motion/1/version/2/permit/', {'yes': 1}) response = self.admin_client.post('/motion/1/version/2/permit/', {'yes': 1})
self.assertRedirects(response, '/motion/1/version/2/') self.assertRedirects(response, '/motion/1/version/2/')
self.assertEqual(self.motion1.active_version, new_version) self.assertEqual(self.motion1.get_active_version(), new_version)
def test_activate_old_version(self): def test_activate_old_version(self):
new_version = self.motion1.last_version new_version = self.motion1.get_last_version()
first_version = self.motion1.versions.order_by('version_number')[0] first_version = self.motion1.versions.order_by('version_number')[0]
self.motion1.set_active_version(new_version) self.motion1.active_version = new_version
self.motion1.save()
self.assertEqual(self.motion1.versions.count(), 2) self.assertEqual(self.motion1.versions.count(), 2)
response = self.admin_client.post('/motion/1/version/1/permit/', {'yes': 1}) response = self.admin_client.post('/motion/1/version/1/permit/', {'yes': 1})
self.motion1 = Motion.objects.get(pk=1) self.motion1 = Motion.objects.get(pk=1)