Added several motion REST API views.
Added motion creation view, motion update view, version permit and delete view, view to support motions, view to set and reset state. Refactored motion submitters and supporters.
This commit is contained in:
parent
d816e0c045
commit
b30afbd635
@ -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