Added several motion REST API views.

Added motion creation view, motion update view, version permit and delete view, view to support motions, view to set and reset state. Refactored motion submitters and supporters.
This commit is contained in:
Norman Jäckel 2015-04-30 19:13:28 +02:00
parent d816e0c045
commit b30afbd635
17 changed files with 734 additions and 164 deletions

View File

@ -104,7 +104,7 @@ class MotionSubmitterMixin(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Fill in the submitter of the motion as default value.""" """Fill in the submitter of the motion as default value."""
if self.motion is not None: if self.motion is not None:
submitter = [submitter.person.id for submitter in self.motion.submitter.all()] submitter = self.motion.submitters.all()
self.initial['submitter'] = submitter self.initial['submitter'] = submitter
super(MotionSubmitterMixin, self).__init__(*args, **kwargs) super(MotionSubmitterMixin, self).__init__(*args, **kwargs)
@ -119,7 +119,7 @@ class MotionSupporterMixin(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Fill in the supporter of the motions as default value.""" """Fill in the supporter of the motions as default value."""
if self.motion is not None: if self.motion is not None:
supporter = [supporter.person.id for supporter in self.motion.supporter.all()] supporter = self.motion.supporters.all()
self.initial['supporter'] = supporter self.initial['supporter'] = supporter
super(MotionSupporterMixin, self).__init__(*args, **kwargs) super(MotionSupporterMixin, self).__init__(*args, **kwargs)

View File

@ -1,3 +1,4 @@
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.db.models import Max from django.db.models import Max
@ -83,6 +84,16 @@ class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
Tags to categorise motions. Tags to categorise motions.
""" """
submitters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_submitters')
"""
Users who submit this motion.
"""
supporters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='motion_supporters')
"""
Users who support this motion.
"""
class Meta: class Meta:
permissions = ( permissions = (
('can_see', ugettext_noop('Can see motions')), ('can_see', ugettext_noop('Can see motions')),
@ -378,55 +389,17 @@ class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
except IndexError: except IndexError:
return self.get_new_version() return self.get_new_version()
@property def is_submitter(self, user):
def submitters(self):
return sorted([object.person for object in self.submitter.all()],
key=lambda person: person.sort_name)
def is_submitter(self, person):
"""Return True, if person is a submitter of this motion. Else: False."""
return self.submitter.filter(person=person.pk).exists()
@property
def supporters(self):
return [supporter.person for supporter in self.supporter.all()]
def add_submitter(self, person):
MotionSubmitter.objects.create(motion=self, person=person)
def clear_submitters(self):
MotionSubmitter.objects.filter(motion=self).delete()
def is_supporter(self, person):
""" """
Return True, if person is a supporter of this motion. Else: False. Returns True if user is a submitter of this motion, else False.
""" """
return self.supporter.filter(person=person.pk).exists() return user in self.submitters.all()
def support(self, person): def is_supporter(self, user):
""" """
Add 'person' as a supporter of this motion. Returns True if user is a supporter of this motion, else False.
""" """
if self.state.allow_support: return user in self.supporters.all()
if not self.is_supporter(person):
MotionSupporter(motion=self, person=person).save()
else:
raise WorkflowError('You can not support a motion in state %s.' % self.state.name)
def unsupport(self, person):
"""
Remove 'person' as supporter from this motion.
"""
if self.state.allow_support:
self.supporter.filter(person=person).delete()
else:
raise WorkflowError('You can not unsupport a motion in state %s.' % self.state.name)
def clear_supporters(self):
"""
Deletes all supporters of this motion.
"""
MotionSupporter.objects.filter(motion=self).delete()
def create_poll(self): def create_poll(self):
""" """
@ -443,6 +416,13 @@ class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
else: else:
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)
@property
def workflow(self):
"""
Returns the id of the workflow of the motion.
"""
return self.state.workflow.pk
def set_state(self, state): def set_state(self, state):
""" """
Set the state of the motion. Set the state of the motion.
@ -504,6 +484,7 @@ class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
* change_state * change_state
* reset_state * reset_state
""" """
# TODO: Remove this method and implement these things in the views.
actions = { actions = {
'see': (person.has_perm('motions.can_see') and 'see': (person.has_perm('motions.can_see') and
(not self.state.required_permission_to_see or (not self.state.required_permission_to_see or
@ -619,46 +600,6 @@ class MotionVersion(RESTModelMixin, AbsoluteUrlMixin, models.Model):
return self.motion return self.motion
class MotionSubmitter(RESTModelMixin, models.Model):
"""Save the submitter of a Motion."""
motion = models.ForeignKey('Motion', related_name="submitter")
"""The motion to witch the object belongs."""
person = models.ForeignKey(User)
"""The user, who is the submitter."""
def __str__(self):
"""Return the name of the submitter as string."""
return str(self.person)
def get_root_rest_element(self):
"""
Returns the motion to this instance which is the root REST element.
"""
return self.motion
class MotionSupporter(RESTModelMixin, models.Model):
"""Save the submitter of a Motion."""
motion = models.ForeignKey('Motion', related_name="supporter")
"""The motion to witch the object belongs."""
person = models.ForeignKey(User)
"""The person, who is the supporter."""
def __str__(self):
"""Return the name of the supporter as string."""
return str(self.person)
def get_root_rest_element(self):
"""
Returns the motion to this instance which is the root REST element.
"""
return self.motion
class Category(RESTModelMixin, AbsoluteUrlMixin, models.Model): class Category(RESTModelMixin, AbsoluteUrlMixin, models.Model):
name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name")) name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name"))
"""Name of the category.""" """Name of the category."""

View File

@ -49,7 +49,7 @@ def motion_to_pdf(pdf, motion):
stylesheet['Heading4'])) stylesheet['Heading4']))
cell1b = [] cell1b = []
cell1b.append(Spacer(0, 0.2 * cm)) cell1b.append(Spacer(0, 0.2 * cm))
for submitter in motion.submitter.all(): for submitter in motion.submitters.all():
cell1b.append(Paragraph(str(submitter), stylesheet['Normal'])) cell1b.append(Paragraph(str(submitter), stylesheet['Normal']))
motion_data.append([cell1a, cell1b]) motion_data.append([cell1a, cell1b])
@ -71,7 +71,7 @@ def motion_to_pdf(pdf, motion):
cell3b = [] cell3b = []
cell3a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font><seqreset id='counter'>" cell3a.append(Paragraph("<font name='Ubuntu-Bold'>%s:</font><seqreset id='counter'>"
% _("Supporters"), stylesheet['Heading4'])) % _("Supporters"), stylesheet['Heading4']))
supporters = motion.supporter.all() supporters = motion.supporters.all()
for supporter in supporters: for supporter in supporters:
cell3b.append(Paragraph("<seq id='counter'/>.&nbsp; %s" % str(supporter), cell3b.append(Paragraph("<seq id='counter'/>.&nbsp; %s" % str(supporter),
stylesheet['Normal'])) stylesheet['Normal']))

View File

@ -3,8 +3,6 @@ from django.utils.translation import ugettext_lazy
from openslides.config.api import config from openslides.config.api import config
from openslides.utils.personal_info import PersonalInfo from openslides.utils.personal_info import PersonalInfo
from .models import Motion
class MotionSubmitterPersonalInfo(PersonalInfo): class MotionSubmitterPersonalInfo(PersonalInfo):
""" """
@ -14,7 +12,7 @@ class MotionSubmitterPersonalInfo(PersonalInfo):
default_weight = 20 default_weight = 20
def get_queryset(self): def get_queryset(self):
return Motion.objects.filter(submitter__person=self.request.user) return None # TODO: Fix this after transforming everything using AngularJS.
class MotionSupporterPersonalInfo(PersonalInfo): class MotionSupporterPersonalInfo(PersonalInfo):
@ -26,7 +24,7 @@ class MotionSupporterPersonalInfo(PersonalInfo):
def get_queryset(self): def get_queryset(self):
if config['motion_min_supporters']: if config['motion_min_supporters']:
return_value = Motion.objects.filter(supporter__person=self.request.user) return_value = None # TODO: Fix this after transforming everything using AngularJS.
else: else:
return_value = None return_value = None
return return_value return return_value

View File

@ -1,4 +1,13 @@
from openslides.utils.rest_api import ModelSerializer, PrimaryKeyRelatedField, SerializerMethodField from django.db import transaction
from django.utils.translation import ugettext as _
from openslides.config.api import config
from openslides.utils.rest_api import (
CharField,
IntegerField,
ModelSerializer,
PrimaryKeyRelatedField,
ValidationError,)
from .models import ( from .models import (
Category, Category,
@ -6,14 +15,20 @@ from .models import (
MotionLog, MotionLog,
MotionOption, MotionOption,
MotionPoll, MotionPoll,
MotionSubmitter,
MotionSupporter,
MotionVersion, MotionVersion,
MotionVote, MotionVote,
State, State,
Workflow,) Workflow,)
def validate_workflow_field(value):
"""
Validator to ensure that the workflow with the given id exists.
"""
if not Workflow.objects.filter(pk=value).exists():
raise ValidationError(_('Workflow %(pk)d does not exist.') % {'pk': value})
class CategorySerializer(ModelSerializer): class CategorySerializer(ModelSerializer):
""" """
Serializer for motion.models.Category objects. Serializer for motion.models.Category objects.
@ -56,24 +71,6 @@ class WorkflowSerializer(ModelSerializer):
fields = ('id', 'name', 'state_set', 'first_state',) fields = ('id', 'name', 'state_set', 'first_state',)
class MotionSubmitterSerializer(ModelSerializer):
"""
Serializer for motion.models.MotionSubmitter objects.
"""
class Meta:
model = MotionSubmitter
fields = ('person',) # TODO: Rename this to 'user', see #1348
class MotionSupporterSerializer(ModelSerializer):
"""
Serializer for motion.models.MotionSupporter objects.
"""
class Meta:
model = MotionSupporter
fields = ('person',) # TODO: Rename this to 'user', see #1348
class MotionLogSerializer(ModelSerializer): class MotionLogSerializer(ModelSerializer):
""" """
Serializer for motion.models.MotionLog objects. Serializer for motion.models.MotionLog objects.
@ -138,36 +135,94 @@ class MotionSerializer(ModelSerializer):
""" """
Serializer for motion.models.Motion objects. Serializer for motion.models.Motion objects.
""" """
versions = MotionVersionSerializer(many=True, read_only=True)
active_version = PrimaryKeyRelatedField(read_only=True) active_version = PrimaryKeyRelatedField(read_only=True)
submitter = MotionSubmitterSerializer(many=True, read_only=True)
supporter = MotionSupporterSerializer(many=True, read_only=True)
state = StateSerializer(read_only=True)
workflow = SerializerMethodField()
polls = MotionPollSerializer(many=True, read_only=True)
log_messages = MotionLogSerializer(many=True, read_only=True) log_messages = MotionLogSerializer(many=True, read_only=True)
polls = MotionPollSerializer(many=True, read_only=True)
reason = CharField(allow_blank=True, required=False, write_only=True)
state = StateSerializer(read_only=True)
text = CharField(write_only=True)
title = CharField(max_length=255, write_only=True)
versions = MotionVersionSerializer(many=True, read_only=True)
workflow = IntegerField(min_value=1, required=False, validators=[validate_workflow_field])
class Meta: class Meta:
model = Motion model = Motion
fields = ( fields = (
'id', 'id',
'identifier', 'identifier',
'identifier_number', 'title',
'parent', 'text',
'category', 'reason',
'tags',
'versions', 'versions',
'active_version', 'active_version',
'submitter', 'parent',
'supporter', 'category',
'submitters',
'supporters',
'state', 'state',
'workflow', 'workflow',
'tags',
'attachments', 'attachments',
'polls', 'polls',
'log_messages',) 'log_messages',)
read_only_fields = ('parent',) # Some other fields are also read_only. See definitions above.
def get_workflow(self, motion): @transaction.atomic
def create(self, validated_data):
""" """
Returns the id of the workflow of the motion. Customized method to create a new motion from some data.
""" """
return motion.state.workflow.pk motion = Motion()
motion.title = validated_data['title']
motion.text = validated_data['text']
motion.reason = validated_data.get('reason', '')
motion.identifier = validated_data.get('identifier')
motion.category = validated_data.get('category')
motion.reset_state(validated_data.get('workflow', int(config['motion_workflow'])))
motion.save()
if validated_data['submitters']:
motion.submitters.add(*validated_data['submitters'])
else:
motion.submitters.add(validated_data['request_user'])
motion.supporters.add(*validated_data['supporters'])
motion.attachments.add(*validated_data['attachments'])
motion.tags.add(*validated_data['tags'])
return motion
@transaction.atomic
def update(self, motion, validated_data):
"""
Customized method to update a motion.
"""
# Identifier and category.
for key in ('identifier', 'category'):
if key in validated_data.keys():
setattr(motion, key, validated_data[key])
# Workflow.
workflow = validated_data.get('workflow')
if workflow is not None and workflow != motion.workflow:
motion.reset_state(workflow)
# Decide if a new version is saved to the database.
if (motion.state.versioning and
not validated_data.get('disable_versioning', False)): # TODO
version = motion.get_new_version()
else:
version = motion.get_last_version()
# Title, text, reason.
for key in ('title', 'text', 'reason'):
if key in validated_data.keys():
setattr(version, key, validated_data[key])
motion.save(use_version=version)
# Submitters, supporters, attachments and tags
for key in ('submitters', 'supporters', 'attachments', 'tags'):
if key in validated_data.keys():
attr = getattr(motion, key)
attr.clear()
attr.add(*validated_data[key])
return motion

View File

@ -196,8 +196,8 @@
<div class="well"> <div class="well">
<!-- Submitter --> <!-- Submitter -->
<h5>{% trans "Submitter" %}:</h5> <h5>{% trans "Submitter" %}:</h5>
{% for submitter in motion.submitter.all %} {% for submitter in motion.submitters.all %}
<a href="{{ submitter.person|absolute_url }}">{{ submitter }}</a>{% if not forloop.last %}, {% endif %} <a href="{{ submitter|absolute_url }}">{{ submitter }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %} {% endfor %}
<!-- Supporters --> <!-- Supporters -->
@ -207,8 +207,8 @@
- -
{% else %} {% else %}
<ol> <ol>
{% for supporter in motion.supporter.all %} {% for supporter in motion.supporters.all %}
<li><a href="{{ supporter.person|absolute_url }}">{{ supporter }}</a></li> <li><a href="{{ supporter|absolute_url }}">{{ supporter }}</a></li>
{% endfor %} {% endfor %}
</ol> </ol>
{% endif %} {% endif %}

View File

@ -98,12 +98,12 @@
<td class="optional">{% if motion.category %}{{ motion.category }}{% else %}{% endif %}</td> <td class="optional">{% if motion.category %}{{ motion.category }}{% else %}{% endif %}</td>
<td class="optional-small"><span class="label label-info">{% trans motion.state.name %}</span></td> <td class="optional-small"><span class="label label-info">{% trans motion.state.name %}</span></td>
<td class="optional"> <td class="optional">
{% for submitter in motion.submitter.all %} {% for submitter in motion.submitters.all %}
{{ submitter.person }}{% if not forloop.last %}, {% endif %} {{ submitter.person }}{% if not forloop.last %}, {% endif %}
{% endfor %} {% endfor %}
</td> </td>
{% if 'motion_min_supporters'|get_config > 0 %} {% if 'motion_min_supporters'|get_config > 0 %}
{% with supporters=motion.supporters|length %} {% with supporters=motion.supporters.all|length %}
<td class="optional"> <td class="optional">
{% if supporters >= 'motion_min_supporters'|get_config %} {% if supporters >= 'motion_min_supporters'|get_config %}
<a class="badge badge-success" rel="tooltip" data-original-title="{% trans 'Enough supporters' %}">{{ supporters }}</a> <a class="badge badge-success" rel="tooltip" data-original-title="{% trans 'Enough supporters' %}">{{ supporters }}</a>

View File

@ -49,14 +49,14 @@
<!-- Submitter --> <!-- Submitter -->
<h4>{% trans "Submitter" %}:</h4> <h4>{% trans "Submitter" %}:</h4>
{% for submitter in motion.submitter.all %} {% for submitter in motion.submitters.all %}
{{ submitter.person }}{% if not forloop.last %},<br>{% endif %} {{ submitter.person }}{% if not forloop.last %},<br>{% endif %}
{% empty %} {% empty %}
- -
{% endfor %} {% endfor %}
<!-- Supporters --> <!-- Supporters -->
{% with motion.supporter.all as supporters %} {% with motion.supporters.all as supporters %}
{% if supporters|length > 0 %} {% if supporters|length > 0 %}
<h4>{% trans "Supporters" %}:</h4> <h4>{% trans "Supporters" %}:</h4>
{% for supporter in supporters %} {% for supporter in supporters %}

View File

@ -2,7 +2,7 @@
{{ object.title }} {{ object.title }}
{{ object.text }} {{ object.text }}
{{ object.reason }} {{ object.reason }}
{{ object.submitters }} {{ object.submitters.all }}
{{ object.supporters }} {{ object.supporters.all }}
{{ object.category }} {{ object.category }}
{{ object.tags.all }} {{ object.tags.all }}

View File

@ -1,12 +1,16 @@
from django.http import Http404
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from reportlab.platypus import SimpleDocTemplate from reportlab.platypus import SimpleDocTemplate
from rest_framework import status
from openslides.utils.rest_api import ModelViewSet from openslides.config.api import config
from openslides.utils.rest_api import ModelViewSet, Response, ValidationError, detail_route
from openslides.utils.views import (PDFView, SingleObjectMixin) from openslides.utils.views import (PDFView, SingleObjectMixin)
from .models import (Category, Motion, MotionPoll, Workflow) from .models import (Category, Motion, MotionPoll, MotionVersion, Workflow)
from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf
from .serializers import CategorySerializer, MotionSerializer, WorkflowSerializer from .serializers import CategorySerializer, MotionSerializer, WorkflowSerializer
@ -21,17 +25,216 @@ class MotionViewSet(ModelViewSet):
def check_permissions(self, request): def check_permissions(self, request):
""" """
Calls self.permission_denied() if the requesting user has not the Calls self.permission_denied() if the requesting user has not the
permission to see motions and in case of create, update or permission to see motions and in case of destroy requests the
destroy requests the permission to manage motions. permission to manage motions.
""" """
# TODO: Use motions.can_create permission and
# motions.can_support permission to create and update some
# objects but restricted concerning the requesting user.
if (not request.user.has_perm('motions.can_see') or if (not request.user.has_perm('motions.can_see') or
(self.action in ('create', 'update', 'destroy') and not (self.action == 'destroy' and not request.user.has_perm('motions.can_manage'))):
request.user.has_perm('motions.can_manage'))):
self.permission_denied(request) self.permission_denied(request)
def create(self, request, *args, **kwargs):
"""
Customized view endpoint to create a new motion.
Checks also whether the requesting user can submit a new motion. He
needs at least the permissions 'motions.can_see' (see
self.check_permission()) and 'motions.can_create'. If the
submitting of new motions by non-staff users is stopped via config
variable 'motion_stop_submitting', the requesting user needs also
to have the permission 'motions.can_manage'.
"""
# Check permissions.
if (not request.user.has_perm('motions.can_create') or
(not config['motion_stop_submitting'] and
not request.user.has_perm('motions.can_manage'))):
self.permission_denied(request)
# Check permission to send submitter and supporter data.
if (not request.user.has_perm('motions.can_manage') and
(request.data.getlist('submitters') or request.data.getlist('supporters'))):
# Non-staff users are not allowed to send submitter or supporter data.
self.permission_denied(request)
# Validate data and create motion.
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
motion = serializer.save(request_user=request.user)
# Write the log message and initiate response.
motion.write_log([ugettext_noop('Motion created')], request.user)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def update(self, request, *args, **kwargs):
"""
Customized view endpoint to update a motion.
Checks also whether the requesting user can update the motion. He
needs at least the permissions 'motions.can_see' (see
self.check_permission()). Also the instance method
get_allowed_actions() is evaluated.
"""
# Get motion.
motion = self.get_object()
# Check permissions.
if not motion.get_allowed_actions(request.user)['update']:
self.permission_denied(request)
# Check permission to send submitter and supporter data.
if (not request.user.has_perm('motions.can_manage') and
(request.data.getlist('submitters') or request.data.getlist('supporters'))):
# Non-staff users are not allowed to send submitter or supporter data.
self.permission_denied(request)
# Validate data and update motion.
serializer = self.get_serializer(
motion,
data=request.data,
partial=kwargs.get('partial', False))
serializer.is_valid(raise_exception=True)
updated_motion = serializer.save()
# Write the log message, check removal of supporters and initiate response.
# TODO: Log if a version was updated.
updated_motion.write_log([ugettext_noop('Motion updated')], request.user)
if (config['motion_remove_supporters'] and updated_motion.state.allow_support and
not request.user.has_perm('motions.can_manage')):
updated_motion.supporters.clear()
updated_motion.write_log([ugettext_noop('All supporters removed')], request.user)
return Response(serializer.data)
@detail_route(methods=['put', 'delete'])
def manage_version(self, request, pk=None):
"""
Special view endpoint to permit and delete a version of a motion.
Send PUT {'version_number': <number>} to permit and DELETE
{'version_number': <number>} to delete a version. Deleting the
active version is not allowed. Only managers can use this view.
"""
# Check permission.
if not request.user.has_perm('motions.can_manage'):
self.permission_denied(request)
# Retrieve motion and version.
motion = self.get_object()
version_number = request.data.get('version_number')
try:
version = motion.versions.get(version_number=version_number)
except MotionVersion.DoesNotExist:
raise Http404('Version %s not found.' % version_number)
# Permit or delete version.
if request.method == 'PUT':
# Permit version.
motion.active_version = version
motion.save(update_fields=['active_version'])
motion.write_log(
message_list=[ugettext_noop('Version'),
' %d ' % version.version_number,
ugettext_noop('permitted')],
person=self.request.user)
message = _('Version %d permitted successfully.') % version.version_number
else:
# Delete version.
# request.method == 'DELETE'
if version == motion.active_version:
raise ValidationError({'detail': _('You can not delete the active version of a motion.')})
version.delete()
motion.write_log(
message_list=[ugettext_noop('Version'),
' %d ' % version.version_number,
ugettext_noop('deleted')],
person=self.request.user)
message = _('Version %d deleted successfully.') % version.version_number
# Initiate response.
return Response({'detail': message})
@detail_route(methods=['post', 'delete'])
def support(self, request, pk=None):
"""
Special view endpoint to support a motion or withdraw support
(unsupport).
Send POST to support and DELETE to unsupport.
Checks also whether the requesting user can do this. He needs at
least the permissions 'motions.can_see' (see
self.check_permission()). Also the the permission
'motions.can_support' is required and the instance method
get_allowed_actions() is evaluated.
"""
# Check permission.
if not request.user.has_perm('motions.can_support'):
self.permission_denied(request)
# Retrieve motion and allowed actions.
motion = self.get_object()
allowed_actions = motion.get_allowed_actions(request.user)
# Support or unsupport motion.
if request.method == 'POST':
# Support motion.
if not allowed_actions['support']:
raise ValidationError({'detail': _('You can not support this motion.')})
motion.supporters.add(request.user)
motion.write_log([ugettext_noop('Motion supported')], request.user)
message = _('You have supported this motion successfully.')
else:
# Unsupport motion.
# request.method == 'DELETE'
if not allowed_actions['unsupport']:
raise ValidationError({'detail': _('You can not unsupport this motion.')})
motion.supporters.remove(request.user)
motion.write_log([ugettext_noop('Motion unsupported')], request.user)
message = _('You have unsupported this motion successfully.')
# Initiate response.
return Response({'detail': message})
@detail_route(methods=['put'])
def set_state(self, request, pk=None):
"""
Special view endpoint to set and reset a state of a motion.
Send PUT {'state': <state_id>} to set and just PUT {} to reset the
state. Only managers can use this view.
"""
# Check permission.
if not request.user.has_perm('motions.can_manage'):
self.permission_denied(request)
# Retrieve motion and state.
motion = self.get_object()
state = request.data.get('state')
# Set or reset state.
if state is not None:
# Check data and set state.
try:
state_id = int(state)
except ValueError:
raise ValidationError({'detail': _('Invalid data. State must be an integer.')})
if state_id not in [item.id for item in motion.state.next_states.all()]:
raise ValidationError(
{'detail': _('You can not set the state to %(state_id)d.') % {'state_id': state_id}})
motion.set_state(state_id)
else:
# Reset state.
motion.reset_state()
# Save motion.
motion.save(update_fields=['state', 'identifier'])
message = _('The state of the motion was set to %s.') % motion.state.name
# Write the log message and initiate response.
motion.write_log(
message_list=[ugettext_noop('State set to'), ' ', motion.state.name],
person=request.user)
return Response({'detail': message})
class PollMixin(object): class PollMixin(object):
""" """

View File

@ -6,6 +6,7 @@ from django.core.urlresolvers import reverse
from rest_framework.decorators import detail_route # noqa from rest_framework.decorators import detail_route # noqa
from rest_framework.serializers import ( # noqa from rest_framework.serializers import ( # noqa
CharField, CharField,
IntegerField,
ListSerializer, ListSerializer,
ModelSerializer, ModelSerializer,
PrimaryKeyRelatedField, PrimaryKeyRelatedField,

View File

@ -0,0 +1,294 @@
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from rest_framework import status
from rest_framework.test import APIClient
from openslides.config.api import config
from openslides.core.models import Tag
from openslides.motions.models import Category, Motion
from openslides.utils.test import TestCase
class CreateMotion(TestCase):
"""
Tests motion creation.
"""
def setUp(self):
self.client.login(username='admin', password='admin')
def test_simple(self):
response = self.client.post(
reverse('motion-list'),
{'title': 'test_title_OoCoo3MeiT9li5Iengu9',
'text': 'test_text_thuoz0iecheiheereiCi'})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
motion = Motion.objects.get()
self.assertEqual(motion.title, 'test_title_OoCoo3MeiT9li5Iengu9')
self.assertEqual(motion.identifier, '1')
self.assertTrue(motion.submitters.exists())
self.assertEqual(motion.submitters.get().username, 'admin')
def test_with_reason(self):
response = self.client.post(
reverse('motion-list'),
{'title': 'test_title_saib4hiHaifo9ohp9yie',
'text': 'test_text_shahhie8Ej4mohvoorie',
'reason': 'test_reason_Ou8GivahYivoh3phoh9c'})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Motion.objects.get().reason, 'test_reason_Ou8GivahYivoh3phoh9c')
def test_without_data(self):
response = self.client.post(
reverse('motion-list'),
{})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'title': ['This field is required.'], 'text': ['This field is required.']})
def test_with_category(self):
category = Category.objects.create(
name='test_category_name_CiengahzooH4ohxietha',
prefix='TEST_PREFIX_la0eadaewuec3seoxeiN')
response = self.client.post(
reverse('motion-list'),
{'title': 'test_title_Air0bahchaiph1ietoo2',
'text': 'test_text_chaeF9wosh8OowazaiVu',
'category': category.pk})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
motion = Motion.objects.get()
self.assertEqual(motion.category, category)
self.assertEqual(motion.identifier, 'TEST_PREFIX_la0eadaewuec3seoxeiN 1')
def test_with_submitters(self):
submitter_1 = get_user_model().objects.create_user(
username='test_username_ooFe6aebei9ieQui2poo',
password='test_password_vie9saiQu5Aengoo9ku0')
submitter_2 = get_user_model().objects.create_user(
username='test_username_eeciengoc4aihie5eeSh',
password='test_password_peik2Eihu5oTh7siequi')
response = self.client.post(
reverse('motion-list'),
{'title': 'test_title_pha7moPh7quoth4paina',
'text': 'test_text_YooGhae6tiangung5Rie',
'submitters': [submitter_1.pk, submitter_2.pk]})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
motion = Motion.objects.get()
self.assertEqual(motion.submitters.count(), 2)
def test_with_one_supporter(self):
supporter = get_user_model().objects.create_user(
username='test_username_ahGhi4Quohyee7ohngie',
password='test_password_Nei6aeh8OhY8Aegh1ohX')
response = self.client.post(
reverse('motion-list'),
{'title': 'test_title_Oecee4Da2Mu9EY6Ui4mu',
'text': 'test_text_FbhgnTFgkbjdmvcjbffg',
'supporters': [supporter.pk]})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
motion = Motion.objects.get()
self.assertEqual(motion.supporters.get().username, 'test_username_ahGhi4Quohyee7ohngie')
def test_with_tag(self):
tag = Tag.objects.create(name='test_tag_iRee3kiecoos4rorohth')
response = self.client.post(
reverse('motion-list'),
{'title': 'test_title_Hahke4loos4eiduNiid9',
'text': 'test_text_johcho0Ucaibiehieghe',
'tags': [tag.pk]})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
motion = Motion.objects.get()
self.assertEqual(motion.tags.get().name, 'test_tag_iRee3kiecoos4rorohth')
def test_with_workflow(self):
self.assertEqual(config['motion_workflow'], '1')
response = self.client.post(
reverse('motion-list'),
{'title': 'test_title_eemuR5hoo4ru2ahgh5EJ',
'text': 'test_text_ohviePopahPhoili7yee',
'workflow': '2'})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
motion = Motion.objects.get()
self.assertEqual(motion.state.workflow.pk, 2)
class UpdateMotion(TestCase):
"""
Tests updating motions.
"""
def setUp(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
self.motion = Motion(
title='test_title_aeng7ahChie3waiR8xoh',
text='test_text_xeigheeha7thopubeu4U')
self.motion.save()
def test_simple_patch(self):
response = self.client.patch(
reverse('motion-detail', args=[self.motion.pk]),
{'identifier': 'test_identifier_jieseghohj7OoSah1Ko9'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
motion = Motion.objects.get()
self.assertEqual(motion.title, 'test_title_aeng7ahChie3waiR8xoh')
self.assertEqual(motion.identifier, 'test_identifier_jieseghohj7OoSah1Ko9')
def test_patch_workflow(self):
self.assertEqual(config['motion_workflow'], '1')
response = self.client.patch(
reverse('motion-detail', args=[self.motion.pk]),
{'workflow': '2'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
motion = Motion.objects.get()
self.assertEqual(motion.title, 'test_title_aeng7ahChie3waiR8xoh')
self.assertEqual(motion.workflow, 2)
def test_patch_supporters(self):
supporter = get_user_model().objects.create_user(
username='test_username_ieB9eicah0uqu6Phoovo',
password='test_password_XaeTe3aesh8ohg6Cohwo')
response = self.client.patch(
reverse('motion-detail', args=[self.motion.pk]),
{'supporters': [supporter.pk]})
self.assertEqual(response.status_code, status.HTTP_200_OK)
motion = Motion.objects.get()
self.assertEqual(motion.title, 'test_title_aeng7ahChie3waiR8xoh')
self.assertEqual(motion.supporters.get().username, 'test_username_ieB9eicah0uqu6Phoovo')
def test_removal_of_supporters(self):
admin = get_user_model().objects.get(username='admin')
group_staff = admin.groups.get(name='Staff')
admin.groups.remove(group_staff)
self.motion.submitters.add(admin)
supporter = get_user_model().objects.create_user(
username='test_username_ahshi4oZin0OoSh9chee',
password='test_password_Sia8ahgeenixu5cei2Ib')
self.motion.supporters.add(supporter)
config['motion_remove_supporters'] = True
self.assertEqual(self.motion.supporters.count(), 1)
response = self.client.patch(
reverse('motion-detail', args=[self.motion.pk]),
{'title': 'new_title_ohph1aedie5Du8sai2ye'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
motion = Motion.objects.get()
self.assertEqual(motion.title, 'new_title_ohph1aedie5Du8sai2ye')
self.assertEqual(motion.supporters.count(), 0)
class ManageVersion(TestCase):
"""
Tests permitting and deleting versions.
"""
def setUp(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
self.motion = Motion(
title='test_title_InieJ5HieZieg1Meid7K',
text='test_text_daePhougho7EenguWe4g')
self.motion.save()
self.version_2 = self.motion.get_new_version(title='new_title_fee7tef0seechazeefiW')
self.motion.save(use_version=self.version_2)
def test_permit(self):
self.assertEqual(Motion.objects.get(pk=self.motion.pk).active_version.version_number, 2)
response = self.client.put(
reverse('motion-manage-version', args=[self.motion.pk]),
{'version_number': '1'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'detail': 'Version 1 permitted successfully.'})
self.assertEqual(Motion.objects.get(pk=self.motion.pk).active_version.version_number, 1)
def test_permit_invalid_version(self):
response = self.client.put(
reverse('motion-manage-version', args=[self.motion.pk]),
{'version_number': '3'})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_delete(self):
response = self.client.delete(
reverse('motion-manage-version', args=[self.motion.pk]),
{'version_number': '1'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'detail': 'Version 1 deleted successfully.'})
self.assertEqual(Motion.objects.get(pk=self.motion.pk).versions.count(), 1)
def test_delete_active_version(self):
response = self.client.delete(
reverse('motion-manage-version', args=[self.motion.pk]),
{'version_number': '2'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'detail': 'You can not delete the active version of a motion.'})
class SupportMotion(TestCase):
"""
Tests supporting a motion.
"""
def setUp(self):
self.admin = get_user_model().objects.get(username='admin')
self.admin.groups.add(3)
self.client.login(username='admin', password='admin')
self.motion = Motion(
title='test_title_chee7ahCha6bingaew4e',
text='test_text_birah1theL9ooseeFaip')
self.motion.save()
def test_support(self):
config['motion_min_supporters'] = 1
response = self.client.post(reverse('motion-support', args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'detail': 'You have supported this motion successfully.'})
def test_unsupport(self):
config['motion_min_supporters'] = 1
self.motion.supporters.add(self.admin)
response = self.client.delete(reverse('motion-support', args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'detail': 'You have unsupported this motion successfully.'})
class SetState(TestCase):
"""
Tests setting a state.
"""
def setUp(self):
self.client = APIClient()
self.client.login(username='admin', password='admin')
self.motion = Motion(
title='test_title_iac4ohquie9Ku6othieC',
text='test_text_Xohphei6Oobee0Evooyu')
self.motion.save()
self.state_id_accepted = 2 # This should be the id of the state 'accepted'.
def test_set_state(self):
response = self.client.put(
reverse('motion-set-state', args=[self.motion.pk]),
{'state': self.state_id_accepted})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'detail': 'The state of the motion was set to accepted.'})
self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.name, 'accepted')
def test_set_state_with_string(self):
# Using a string is not allowed even if it is the correct name of the state.
response = self.client.put(
reverse('motion-set-state', args=[self.motion.pk]),
{'state': 'accepted'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'detail': 'Invalid data. State must be an integer.'})
def test_set_unknown_state(self):
invalid_state_id = 0
response = self.client.put(
reverse('motion-set-state', args=[self.motion.pk]),
{'state': invalid_state_id})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, {'detail': 'You can not set the state to %d.' % invalid_state_id})
def test_reset(self):
self.motion.set_state(self.state_id_accepted)
self.motion.save()
response = self.client.put(reverse('motion-set-state', args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'detail': 'The state of the motion was set to submitted.'})
self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.name, 'submitted')

View File

@ -49,8 +49,8 @@ class CSVImport(TestCase):
self.assertEqual(motion1.title, u'Entlastung des Vorstandes') self.assertEqual(motion1.title, u'Entlastung des Vorstandes')
self.assertEqual(motion1.text, u'Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten.') self.assertEqual(motion1.text, u'Die Versammlung möge beschließen, den Vorstand für seine letzte Legislaturperiode zu entlasten.')
self.assertEqual(motion1.reason, u'Bericht erfolgt mündlich.') self.assertEqual(motion1.reason, u'Bericht erfolgt mündlich.')
self.assertEqual(len(motion1.submitter.all()), 1) self.assertEqual(len(motion1.submitters.all()), 1)
self.assertEqual(motion1.submitter.all()[0].person, self.normal_user) self.assertEqual(motion1.submitters.all()[0], self.normal_user)
self.assertTrue(motion1.category is None) self.assertTrue(motion1.category is None)
self.assertTrue('Submitter unknown.' in warning_message) self.assertTrue('Submitter unknown.' in warning_message)
self.assertTrue('Category unknown.' in warning_message) self.assertTrue('Category unknown.' in warning_message)
@ -61,8 +61,8 @@ class CSVImport(TestCase):
self.assertHTMLEqual(motion2.text, u'''<p>Die Versammlung möge beschließen, die Satzung in § 2 Abs. 3 wie folgt zu ändern:</p> self.assertHTMLEqual(motion2.text, u'''<p>Die Versammlung möge beschließen, die Satzung in § 2 Abs. 3 wie folgt zu ändern:</p>
<p>Es wird vor dem Wort "Zweck" das Wort "gemeinnütziger" eingefügt.</p>''') <p>Es wird vor dem Wort "Zweck" das Wort "gemeinnütziger" eingefügt.</p>''')
self.assertEqual(motion2.reason, u'Die Änderung der Satzung ist aufgrund der letzten Erfahrungen eine sinnvolle Maßnahme, weil ...') self.assertEqual(motion2.reason, u'Die Änderung der Satzung ist aufgrund der letzten Erfahrungen eine sinnvolle Maßnahme, weil ...')
self.assertEqual(len(motion2.submitter.all()), 1) self.assertEqual(len(motion2.submitters.all()), 1)
self.assertEqual(motion2.submitter.all()[0].person, special_user) self.assertEqual(motion2.submitters.all()[0], special_user)
self.assertEqual(motion2.category.name, u"Satzungsanträge") # category is created automatically self.assertEqual(motion2.category.name, u"Satzungsanträge") # category is created automatically
# check user 'John Doe' # check user 'John Doe'

View File

@ -66,11 +66,10 @@ class ModelTest(TestCase):
def test_supporter(self): def test_supporter(self):
self.assertFalse(self.motion.is_supporter(self.test_user)) self.assertFalse(self.motion.is_supporter(self.test_user))
self.motion.support(self.test_user) self.motion.supporters.add(self.test_user)
self.assertTrue(self.motion.is_supporter(self.test_user)) self.assertTrue(self.motion.is_supporter(self.test_user))
self.motion.unsupport(self.test_user) self.motion.supporters.remove(self.test_user)
self.assertFalse(self.motion.is_supporter(self.test_user)) self.assertFalse(self.motion.is_supporter(self.test_user))
self.motion.unsupport(self.test_user)
def test_poll(self): def test_poll(self):
self.motion.state = State.objects.get(pk=1) self.motion.state = State.objects.get(pk=1)
@ -89,10 +88,8 @@ class ModelTest(TestCase):
self.motion.state = State.objects.get(pk=6) self.motion.state = State.objects.get(pk=6)
self.assertEqual(self.motion.state.name, 'permitted') self.assertEqual(self.motion.state.name, 'permitted')
self.assertEqual(self.motion.state.get_action_word(), 'Permit') self.assertEqual(self.motion.state.get_action_word(), 'Permit')
with self.assertRaises(WorkflowError): self.assertFalse(self.motion.get_allowed_actions(self.test_user)['support'])
self.motion.support(self.test_user) self.assertFalse(self.motion.get_allowed_actions(self.test_user)['unsupport'])
with self.assertRaises(WorkflowError):
self.motion.unsupport(self.test_user)
def test_new_states_or_workflows(self): def test_new_states_or_workflows(self):
workflow_1 = Workflow.objects.create(name='W1') workflow_1 = Workflow.objects.create(name='W1')

View File

@ -135,7 +135,7 @@ class TestMotionDetailView(MotionViewTestCase):
def test_get_without_required_permission_from_state_but_by_submitter(self): def test_get_without_required_permission_from_state_but_by_submitter(self):
self.motion1.state.required_permission_to_see = 'motions.can_manage' self.motion1.state.required_permission_to_see = 'motions.can_manage'
self.motion1.state.save() self.motion1.state.save()
self.motion1.add_submitter(self.registered) self.motion1.submitters.add(self.registered)
self.check_url('/motions/1/', self.registered_client, 200) self.check_url('/motions/1/', self.registered_client, 200)
@ -360,7 +360,7 @@ class TestMotionUpdateView(MotionViewTestCase):
'reason': 'motion reason'}) 'reason': 'motion reason'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
motion = Motion.objects.get(pk=1) motion = Motion.objects.get(pk=1)
motion.add_submitter(self.delegate) motion.submitters.add(self.delegate)
response = self.delegate_client.post(self.url, {'title': 'my title', response = self.delegate_client.post(self.url, {'title': 'my title',
'text': 'motion text', 'text': 'motion text',
'reason': 'motion reason'}) 'reason': 'motion reason'})
@ -468,7 +468,7 @@ class TestMotionUpdateView(MotionViewTestCase):
'text': 'eequei1Tee1aegeNgee0', 'text': 'eequei1Tee1aegeNgee0',
'submitter': self.delegate.id}) 'submitter': self.delegate.id})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
motion.add_submitter(self.delegate) motion.submitters.add(self.delegate)
# Edit three times, without removal of supporters, with removal and in another state # Edit three times, without removal of supporters, with removal and in another state
for i in range(3): for i in range(3):
@ -480,9 +480,9 @@ class TestMotionUpdateView(MotionViewTestCase):
'text': 'Lohjuu1aebewiu2or3oh'}) 'text': 'Lohjuu1aebewiu2or3oh'})
self.assertRedirects(response, '/motions/%s/' % motion.id) self.assertRedirects(response, '/motions/%s/' % motion.id)
if i == 0 or i == 2: if i == 0 or i == 2:
self.assertTrue(self.registered in Motion.objects.get(pk=motion.pk).supporters) self.assertTrue(self.registered in Motion.objects.get(pk=motion.pk).supporters.all())
else: else:
self.assertFalse(self.registered in Motion.objects.get(pk=motion.pk).supporters) self.assertFalse(self.registered in Motion.objects.get(pk=motion.pk).supporters.all())
# Preparing the comming (third) run # Preparing the comming (third) run
motion = Motion.objects.get(pk=motion.pk) motion = Motion.objects.get(pk=motion.pk)
motion.support(self.registered) motion.support(self.registered)
@ -577,7 +577,7 @@ class TestMotionDeleteView(MotionViewTestCase):
def test_delegate(self): def test_delegate(self):
response = self.delegate_client.post('/motions/2/del/', {'yes': 'yes'}) response = self.delegate_client.post('/motions/2/del/', {'yes': 'yes'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
Motion.objects.get(pk=2).add_submitter(self.delegate) Motion.objects.get(pk=2).submitters.add(self.delegate)
response = self.delegate_client.post('/motions/2/del/', {'yes': 'yes'}) response = self.delegate_client.post('/motions/2/del/', {'yes': 'yes'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)

View File

View File

@ -0,0 +1,81 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch
from rest_framework.exceptions import PermissionDenied
from openslides.motions.views import MotionViewSet
class MotionViewSetCreate(TestCase):
"""
Tests create view of MotionViewSet.
"""
def setUp(self):
self.request = MagicMock()
self.view_instance = MotionViewSet()
self.view_instance.request = self.request
self.view_instance.format_kwarg = MagicMock()
self.view_instance.get_serializer = get_serializer_mock = MagicMock()
get_serializer_mock.return_value = self.mock_serializer = MagicMock()
@patch('openslides.motions.views.config')
def test_simple_create(self, mock_config):
self.request.user.has_perm.return_value = True
self.view_instance.create(self.request)
self.mock_serializer.save.assert_called_with(request_user=self.request.user)
@patch('openslides.motions.views.config')
def test_user_without_can_create_perm(self, mock_config):
self.request.user.has_perm.return_value = False
with self.assertRaises(PermissionDenied):
self.view_instance.create(self.request)
class MotionViewSetUpdate(TestCase):
"""
Tests update view of MotionViewSet.
"""
def setUp(self):
self.request = MagicMock()
self.view_instance = MotionViewSet()
self.view_instance.request = self.request
self.view_instance.kwargs = MagicMock()
self.view_instance.get_object = MagicMock()
self.view_instance.get_serializer = get_serializer_mock = MagicMock()
get_serializer_mock.return_value = self.mock_serializer = MagicMock()
@patch('openslides.motions.views.config')
def test_simple_update(self, mock_config):
self.request.user.has_perm.return_value = True
self.view_instance.update(self.request)
self.mock_serializer.save.assert_called_with()
@patch('openslides.motions.views.config')
def test_user_without_perms(self, mock_config):
self.request.user.has_perm.return_value = False
with self.assertRaises(PermissionDenied):
self.view_instance.update(self.request)
class MotionViewSetManageVersion(TestCase):
"""
Tests views of MotionViewSet to manage versions.
"""
def setUp(self):
self.request = MagicMock()
self.view_instance = MotionViewSet()
self.view_instance.request = self.request
self.view_instance.get_object = get_object_mock = MagicMock()
get_object_mock.return_value = self.mock_motion = MagicMock()
def test_activate_version(self):
self.request.method = 'PUT'
self.request.user.has_perm.return_value = True
self.view_instance.manage_version(self.request)
self.mock_motion.save.assert_called_with(update_fields=['active_version'])
def test_delete_version(self):
self.request.method = 'DELETE'
self.request.user.has_perm.return_value = True
self.view_instance.manage_version(self.request)
self.mock_motion.versions.get.return_value.delete.assert_called_with()