Merge pull request #1517 from normanjaeckel/MotionRESTAPIChanges
Added motion views.
This commit is contained in:
commit
9c51313a82
@ -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)
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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'/>. %s" % str(supporter),
|
cell3b.append(Paragraph("<seq id='counter'/>. %s" % str(supporter),
|
||||||
stylesheet['Normal']))
|
stylesheet['Normal']))
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 }}
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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,
|
||||||
|
294
tests/integration/motions/test_viewset.py
Normal file
294
tests/integration/motions/test_viewset.py
Normal 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')
|
@ -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'
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
0
tests/unit/motions/__init__.py
Normal file
0
tests/unit/motions/__init__.py
Normal file
81
tests/unit/motions/test_views.py
Normal file
81
tests/unit/motions/test_views.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user