Merge pull request #1517 from normanjaeckel/MotionRESTAPIChanges

Added motion views.
This commit is contained in:
Oskar Hahn 2015-05-18 07:26:00 +02:00
commit 9c51313a82
17 changed files with 734 additions and 164 deletions

View File

@ -104,7 +104,7 @@ class MotionSubmitterMixin(forms.ModelForm):
def __init__(self, *args, **kwargs):
"""Fill in the submitter of the motion as default value."""
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
super(MotionSubmitterMixin, self).__init__(*args, **kwargs)
@ -119,7 +119,7 @@ class MotionSupporterMixin(forms.ModelForm):
def __init__(self, *args, **kwargs):
"""Fill in the supporter of the motions as default value."""
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
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.db import models
from django.db.models import Max
@ -83,6 +84,16 @@ class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
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:
permissions = (
('can_see', ugettext_noop('Can see motions')),
@ -378,55 +389,17 @@ class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
except IndexError:
return self.get_new_version()
@property
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):
def is_submitter(self, user):
"""
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:
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()
return user in self.supporters.all()
def create_poll(self):
"""
@ -443,6 +416,13 @@ class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
else:
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):
"""
Set the state of the motion.
@ -504,6 +484,7 @@ class Motion(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, models.Model):
* change_state
* reset_state
"""
# TODO: Remove this method and implement these things in the views.
actions = {
'see': (person.has_perm('motions.can_see') and
(not self.state.required_permission_to_see or
@ -619,46 +600,6 @@ class MotionVersion(RESTModelMixin, AbsoluteUrlMixin, models.Model):
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):
name = models.CharField(max_length=255, verbose_name=ugettext_lazy("Category name"))
"""Name of the category."""

View File

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

View File

@ -3,8 +3,6 @@ from django.utils.translation import ugettext_lazy
from openslides.config.api import config
from openslides.utils.personal_info import PersonalInfo
from .models import Motion
class MotionSubmitterPersonalInfo(PersonalInfo):
"""
@ -14,7 +12,7 @@ class MotionSubmitterPersonalInfo(PersonalInfo):
default_weight = 20
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):
@ -26,7 +24,7 @@ class MotionSupporterPersonalInfo(PersonalInfo):
def get_queryset(self):
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:
return_value = None
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 (
Category,
@ -6,14 +15,20 @@ from .models import (
MotionLog,
MotionOption,
MotionPoll,
MotionSubmitter,
MotionSupporter,
MotionVersion,
MotionVote,
State,
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):
"""
Serializer for motion.models.Category objects.
@ -56,24 +71,6 @@ class WorkflowSerializer(ModelSerializer):
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):
"""
Serializer for motion.models.MotionLog objects.
@ -138,36 +135,94 @@ class MotionSerializer(ModelSerializer):
"""
Serializer for motion.models.Motion objects.
"""
versions = MotionVersionSerializer(many=True, 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)
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:
model = Motion
fields = (
'id',
'identifier',
'identifier_number',
'parent',
'category',
'tags',
'title',
'text',
'reason',
'versions',
'active_version',
'submitter',
'supporter',
'parent',
'category',
'submitters',
'supporters',
'state',
'workflow',
'tags',
'attachments',
'polls',
'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">
<!-- Submitter -->
<h5>{% trans "Submitter" %}:</h5>
{% for submitter in motion.submitter.all %}
<a href="{{ submitter.person|absolute_url }}">{{ submitter }}</a>{% if not forloop.last %}, {% endif %}
{% for submitter in motion.submitters.all %}
<a href="{{ submitter|absolute_url }}">{{ submitter }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
<!-- Supporters -->
@ -207,8 +207,8 @@
-
{% else %}
<ol>
{% for supporter in motion.supporter.all %}
<li><a href="{{ supporter.person|absolute_url }}">{{ supporter }}</a></li>
{% for supporter in motion.supporters.all %}
<li><a href="{{ supporter|absolute_url }}">{{ supporter }}</a></li>
{% endfor %}
</ol>
{% endif %}

View File

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

View File

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

View File

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

View File

@ -1,12 +1,16 @@
from django.http import Http404
from django.utils.text import slugify
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop
from django.shortcuts import get_object_or_404
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 .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 .serializers import CategorySerializer, MotionSerializer, WorkflowSerializer
@ -21,17 +25,216 @@ class MotionViewSet(ModelViewSet):
def check_permissions(self, request):
"""
Calls self.permission_denied() if the requesting user has not the
permission to see motions and in case of create, update or
destroy requests the permission to manage motions.
permission to see motions and in case of destroy requests the
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
(self.action in ('create', 'update', 'destroy') and not
request.user.has_perm('motions.can_manage'))):
(self.action == 'destroy' and not request.user.has_perm('motions.can_manage'))):
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):
"""

View File

@ -6,6 +6,7 @@ from django.core.urlresolvers import reverse
from rest_framework.decorators import detail_route # noqa
from rest_framework.serializers import ( # noqa
CharField,
IntegerField,
ListSerializer,
ModelSerializer,
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.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(len(motion1.submitter.all()), 1)
self.assertEqual(motion1.submitter.all()[0].person, self.normal_user)
self.assertEqual(len(motion1.submitters.all()), 1)
self.assertEqual(motion1.submitters.all()[0], self.normal_user)
self.assertTrue(motion1.category is None)
self.assertTrue('Submitter 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>
<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(len(motion2.submitter.all()), 1)
self.assertEqual(motion2.submitter.all()[0].person, special_user)
self.assertEqual(len(motion2.submitters.all()), 1)
self.assertEqual(motion2.submitters.all()[0], special_user)
self.assertEqual(motion2.category.name, u"Satzungsanträge") # category is created automatically
# check user 'John Doe'

View File

@ -66,11 +66,10 @@ class ModelTest(TestCase):
def test_supporter(self):
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.motion.unsupport(self.test_user)
self.motion.supporters.remove(self.test_user)
self.assertFalse(self.motion.is_supporter(self.test_user))
self.motion.unsupport(self.test_user)
def test_poll(self):
self.motion.state = State.objects.get(pk=1)
@ -89,10 +88,8 @@ class ModelTest(TestCase):
self.motion.state = State.objects.get(pk=6)
self.assertEqual(self.motion.state.name, 'permitted')
self.assertEqual(self.motion.state.get_action_word(), 'Permit')
with self.assertRaises(WorkflowError):
self.motion.support(self.test_user)
with self.assertRaises(WorkflowError):
self.motion.unsupport(self.test_user)
self.assertFalse(self.motion.get_allowed_actions(self.test_user)['support'])
self.assertFalse(self.motion.get_allowed_actions(self.test_user)['unsupport'])
def test_new_states_or_workflows(self):
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):
self.motion1.state.required_permission_to_see = 'motions.can_manage'
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)
@ -360,7 +360,7 @@ class TestMotionUpdateView(MotionViewTestCase):
'reason': 'motion reason'})
self.assertEqual(response.status_code, 403)
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',
'text': 'motion text',
'reason': 'motion reason'})
@ -468,7 +468,7 @@ class TestMotionUpdateView(MotionViewTestCase):
'text': 'eequei1Tee1aegeNgee0',
'submitter': self.delegate.id})
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
for i in range(3):
@ -480,9 +480,9 @@ class TestMotionUpdateView(MotionViewTestCase):
'text': 'Lohjuu1aebewiu2or3oh'})
self.assertRedirects(response, '/motions/%s/' % motion.id)
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:
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
motion = Motion.objects.get(pk=motion.pk)
motion.support(self.registered)
@ -577,7 +577,7 @@ class TestMotionDeleteView(MotionViewTestCase):
def test_delegate(self):
response = self.delegate_client.post('/motions/2/del/', {'yes': 'yes'})
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'})
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()