diff --git a/openslides/motions/forms.py b/openslides/motions/forms.py index 432020a74..048480e79 100644 --- a/openslides/motions/forms.py +++ b/openslides/motions/forms.py @@ -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) diff --git a/openslides/motions/models.py b/openslides/motions/models.py index bdc807308..80b109128 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -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.""" diff --git a/openslides/motions/pdf.py b/openslides/motions/pdf.py index d7c930a6b..7b70bb250 100644 --- a/openslides/motions/pdf.py +++ b/openslides/motions/pdf.py @@ -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("%s:" % _("Supporters"), stylesheet['Heading4'])) - supporters = motion.supporter.all() + supporters = motion.supporters.all() for supporter in supporters: cell3b.append(Paragraph(".  %s" % str(supporter), stylesheet['Normal'])) diff --git a/openslides/motions/personal_info.py b/openslides/motions/personal_info.py index 147acbfcf..8d7df7298 100644 --- a/openslides/motions/personal_info.py +++ b/openslides/motions/personal_info.py @@ -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 diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index d8566aedd..8802d1455 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -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 diff --git a/openslides/motions/templates/motions/motion_detail.html b/openslides/motions/templates/motions/motion_detail.html index 52c0102fd..d71e01ecb 100644 --- a/openslides/motions/templates/motions/motion_detail.html +++ b/openslides/motions/templates/motions/motion_detail.html @@ -196,8 +196,8 @@
{% trans "Submitter" %}:
- {% for submitter in motion.submitter.all %} - {{ submitter }}{% if not forloop.last %}, {% endif %} + {% for submitter in motion.submitters.all %} + {{ submitter }}{% if not forloop.last %}, {% endif %} {% endfor %} @@ -207,8 +207,8 @@ - {% else %}
    - {% for supporter in motion.supporter.all %} -
  1. {{ supporter }}
  2. + {% for supporter in motion.supporters.all %} +
  3. {{ supporter }}
  4. {% endfor %}
{% endif %} diff --git a/openslides/motions/templates/motions/motion_list.html b/openslides/motions/templates/motions/motion_list.html index 7135690b2..4eb6e0060 100644 --- a/openslides/motions/templates/motions/motion_list.html +++ b/openslides/motions/templates/motions/motion_list.html @@ -98,12 +98,12 @@ {% if motion.category %}{{ motion.category }}{% else %}–{% endif %} {% trans motion.state.name %} - {% for submitter in motion.submitter.all %} + {% for submitter in motion.submitters.all %} {{ submitter.person }}{% if not forloop.last %}, {% endif %} {% endfor %} {% if 'motion_min_supporters'|get_config > 0 %} - {% with supporters=motion.supporters|length %} + {% with supporters=motion.supporters.all|length %} {% if supporters >= 'motion_min_supporters'|get_config %} {{ supporters }} diff --git a/openslides/motions/templates/motions/slide.html b/openslides/motions/templates/motions/slide.html index afbbfa63e..ea3f9fe94 100644 --- a/openslides/motions/templates/motions/slide.html +++ b/openslides/motions/templates/motions/slide.html @@ -49,14 +49,14 @@

{% trans "Submitter" %}:

- {% for submitter in motion.submitter.all %} + {% for submitter in motion.submitters.all %} {{ submitter.person }}{% if not forloop.last %},
{% endif %} {% empty %} - {% endfor %} - {% with motion.supporter.all as supporters %} + {% with motion.supporters.all as supporters %} {% if supporters|length > 0 %}

{% trans "Supporters" %}:

{% for supporter in supporters %} diff --git a/openslides/motions/templates/search/indexes/motions/motion_text.txt b/openslides/motions/templates/search/indexes/motions/motion_text.txt index d6261e605..b27bd5d81 100644 --- a/openslides/motions/templates/search/indexes/motions/motion_text.txt +++ b/openslides/motions/templates/search/indexes/motions/motion_text.txt @@ -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 }} diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 003c830b6..915b92f96 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -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': } to permit and DELETE + {'version_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': } 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): """ diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index 53aad5b4a..cb24813c0 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -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, diff --git a/tests/integration/motions/test_viewset.py b/tests/integration/motions/test_viewset.py new file mode 100644 index 000000000..78b515439 --- /dev/null +++ b/tests/integration/motions/test_viewset.py @@ -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') diff --git a/tests/old/motions/test_csv_import.py b/tests/old/motions/test_csv_import.py index 4f58e5be3..2fc0c58d9 100644 --- a/tests/old/motions/test_csv_import.py +++ b/tests/old/motions/test_csv_import.py @@ -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'''

Die Versammlung möge beschließen, die Satzung in § 2 Abs. 3 wie folgt zu ändern:

Es wird vor dem Wort "Zweck" das Wort "gemeinnütziger" eingefügt.

''') 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' diff --git a/tests/old/motions/test_models.py b/tests/old/motions/test_models.py index 50bf3f177..85b5dcb3d 100644 --- a/tests/old/motions/test_models.py +++ b/tests/old/motions/test_models.py @@ -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') diff --git a/tests/old/motions/test_views.py b/tests/old/motions/test_views.py index d693d5f53..fc3d0cc02 100644 --- a/tests/old/motions/test_views.py +++ b/tests/old/motions/test_views.py @@ -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) diff --git a/tests/unit/motions/__init__.py b/tests/unit/motions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/motions/test_views.py b/tests/unit/motions/test_views.py new file mode 100644 index 000000000..f3cbe49be --- /dev/null +++ b/tests/unit/motions/test_views.py @@ -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()