Merge pull request #2344 from normanjaeckel/MotionRecommendation
Added recommendations for motions.
This commit is contained in:
commit
f2c3e535a5
@ -25,6 +25,8 @@ Motions:
|
||||
- Added button to sort and number all motions in a category.
|
||||
- Introduced pdfMake for clientside generation of PDFs.
|
||||
- Added configurable fields for comments.
|
||||
- Added recommendations for motions.
|
||||
- Changed label of former state "commited a bill" to "refered to committee".
|
||||
|
||||
Users:
|
||||
- Added field is_committee and new default group Committees.
|
||||
|
@ -27,6 +27,7 @@ def get_config_variables():
|
||||
'value': "WITHOUT_ABSTAIN",
|
||||
'display_name': 'Yes and No votes'},)
|
||||
PERCENT_BASE_CHOICES_MOTION += PERCENT_BASE_CHOICES
|
||||
|
||||
# General
|
||||
yield ConfigVariable(
|
||||
name='motions_workflow',
|
||||
@ -102,6 +103,16 @@ def get_config_variables():
|
||||
group='Motions',
|
||||
subgroup='General')
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_recommendations_by',
|
||||
default_value='Recommendation committee',
|
||||
label='Name of recommendation committee',
|
||||
help_text='Use an empty value to disable the recommendation system.',
|
||||
weight=332,
|
||||
group='Motions',
|
||||
subgroup='General',
|
||||
translatable=True)
|
||||
|
||||
# Amendments
|
||||
yield ConfigVariable(
|
||||
name='motions_amendments_enabled',
|
||||
|
86
openslides/motions/migrations/0004_auto_20160907_2343.py
Normal file
86
openslides/motions/migrations/0004_auto_20160907_2343.py
Normal file
@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.8 on 2016-09-07 23:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def change_label_of_state(apps, schema_editor):
|
||||
"""
|
||||
Changes the label of former state "commited a bill" to "refered to committee".
|
||||
"""
|
||||
# Disconnect autoupdate. We do not want to trigger it here.
|
||||
models.signals.post_save.disconnect(dispatch_uid='inform_changed_data_receiver')
|
||||
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
State = apps.get_model('motions', 'State')
|
||||
|
||||
try:
|
||||
state = State.objects.get(name='commited a bill')
|
||||
except State.DoesNotExist:
|
||||
# State does not exists, there is nothing to change.
|
||||
pass
|
||||
else:
|
||||
state.name = 'refered to committee'
|
||||
state.action_word = 'Refer to committee'
|
||||
state.save()
|
||||
|
||||
|
||||
def add_recommendation_labels(apps, schema_editor):
|
||||
"""
|
||||
Adds recommendation labels to some of the built-in states.
|
||||
"""
|
||||
# Disconnect autoupdate. We do not want to trigger it here.
|
||||
models.signals.post_save.disconnect(dispatch_uid='inform_changed_data_receiver')
|
||||
|
||||
# We get the model from the versioned app registry;
|
||||
# if we directly import it, it will be the wrong version.
|
||||
State = apps.get_model('motions', 'State')
|
||||
|
||||
name_label_map = {
|
||||
'accepted': 'Acceptance',
|
||||
'rejected': 'Rejection',
|
||||
'not decided': 'No decision',
|
||||
'permitted': 'Permission',
|
||||
'adjourned': 'Adjournment',
|
||||
'not concerned': 'No concernment',
|
||||
'refered to committee': 'Referral to committee',
|
||||
'rejected (not authorized)': 'Rejection (not authorized)',
|
||||
}
|
||||
for state in State.objects.all():
|
||||
if name_label_map.get(state.name):
|
||||
state.recommendation_label = name_label_map[state.name]
|
||||
state.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('motions', '0003_auto_20160819_0925'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='motion',
|
||||
name='recommendation',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='recommendation_label',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='motion',
|
||||
name='state',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='motions.State'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
change_label_of_state
|
||||
),
|
||||
migrations.RunPython(
|
||||
add_recommendation_labels
|
||||
),
|
||||
]
|
@ -51,6 +51,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
|
||||
state = models.ForeignKey(
|
||||
'State',
|
||||
related_name='+',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True) # TODO: Check whether null=True is necessary.
|
||||
"""
|
||||
@ -59,6 +60,15 @@ class Motion(RESTModelMixin, models.Model):
|
||||
This attribute is to get the current state of the motion.
|
||||
"""
|
||||
|
||||
recommendation = models.ForeignKey(
|
||||
'State',
|
||||
related_name='+',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True)
|
||||
"""
|
||||
The recommendation of a person or committee for this motion.
|
||||
"""
|
||||
|
||||
identifier = models.CharField(max_length=255, null=True, blank=True,
|
||||
unique=True)
|
||||
"""
|
||||
@ -471,6 +481,16 @@ class Motion(RESTModelMixin, models.Model):
|
||||
Workflow.objects.get(pk=config['motions_workflow']).states.all()[0])
|
||||
self.set_state(new_state)
|
||||
|
||||
def set_recommendation(self, recommendation):
|
||||
"""
|
||||
Set the recommendation of the motion.
|
||||
|
||||
'recommendation' can be the id of a state object or a state object.
|
||||
"""
|
||||
if type(recommendation) is int:
|
||||
recommendation = State.objects.get(pk=recommendation)
|
||||
self.recommendation = recommendation
|
||||
|
||||
def get_agenda_title(self):
|
||||
"""
|
||||
Return a simple title string for the agenda.
|
||||
@ -523,6 +543,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
* unsupport
|
||||
* change_state
|
||||
* reset_state
|
||||
* change_recommendation
|
||||
|
||||
NOTE: If you update this function please also update the
|
||||
'isAllowed' function on client side in motions/site.js.
|
||||
@ -553,7 +574,11 @@ class Motion(RESTModelMixin, models.Model):
|
||||
|
||||
'change_state': person.has_perm('motions.can_manage'),
|
||||
|
||||
'reset_state': person.has_perm('motions.can_manage')}
|
||||
'reset_state': person.has_perm('motions.can_manage'),
|
||||
|
||||
'change_recommendation': person.has_perm('motions.can_manage'),
|
||||
|
||||
}
|
||||
|
||||
actions['edit'] = actions['update']
|
||||
|
||||
@ -800,11 +825,16 @@ class State(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Defines a state for a motion.
|
||||
|
||||
Every state belongs to a workflow. All states of a workflow are linked together
|
||||
via 'next_states'. One of these states is the first state, but this
|
||||
is saved in the workflow table (one-to-one relation). In every state
|
||||
you can configure some handling of a motion. See the following fields
|
||||
for more information.
|
||||
Every state belongs to a workflow. All states of a workflow are linked
|
||||
together via 'next_states'. One of these states is the first state, but
|
||||
this is saved in the workflow table (one-to-one relation). In every
|
||||
state you can configure some handling of a motion. See the following
|
||||
fields for more information.
|
||||
|
||||
Additionally every motion can refer to one state as recommendation of
|
||||
an person or committee (see config 'motions_recommendations_by'). This
|
||||
means that the person or committee recommends to set the motion to this
|
||||
state.
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
@ -813,6 +843,9 @@ class State(RESTModelMixin, models.Model):
|
||||
action_word = models.CharField(max_length=255)
|
||||
"""An alternative string to be used for a button to switch to this state."""
|
||||
|
||||
recommendation_label = models.CharField(max_length=255, null=True)
|
||||
"""A string for a recommendation to set the motion to this state."""
|
||||
|
||||
workflow = models.ForeignKey(
|
||||
'Workflow',
|
||||
on_delete=models.CASCADE,
|
||||
@ -876,9 +909,13 @@ class State(RESTModelMixin, models.Model):
|
||||
def save(self, **kwargs):
|
||||
"""Saves a state in the database.
|
||||
|
||||
Used to check the integrity before saving.
|
||||
Used to check the integrity before saving. Also used to check that
|
||||
recommendation_label is not an empty string.
|
||||
"""
|
||||
self.check_next_states()
|
||||
if self.recommendation_label == '':
|
||||
raise WorkflowError('The field recommendation_label of {} must not '
|
||||
'be an empty string.'.format(self))
|
||||
super(State, self).save(**kwargs)
|
||||
|
||||
def get_action_word(self):
|
||||
|
@ -51,6 +51,7 @@ class StateSerializer(ModelSerializer):
|
||||
'id',
|
||||
'name',
|
||||
'action_word',
|
||||
'recommendation_label',
|
||||
'css_class',
|
||||
'required_permission_to_see',
|
||||
'allow_support',
|
||||
@ -263,12 +264,13 @@ class MotionSerializer(ModelSerializer):
|
||||
'comments',
|
||||
'state',
|
||||
'workflow_id',
|
||||
'recommendation',
|
||||
'tags',
|
||||
'attachments',
|
||||
'polls',
|
||||
'agenda_item_id',
|
||||
'log_messages',)
|
||||
read_only_fields = ('state',) # Some other fields are also read_only. See definitions above.
|
||||
read_only_fields = ('state', 'recommendation',) # Some other fields are also read_only. See definitions above.
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
|
@ -6,8 +6,8 @@ from .models import State, Workflow
|
||||
def create_builtin_workflows(sender, **kwargs):
|
||||
"""
|
||||
Receiver function to create a simple and a complex workflow. It is
|
||||
connected to the signal openslides.core.signals.post_database_setup
|
||||
during app loading.
|
||||
connected to the signal django.db.models.signals.post_migrate during
|
||||
app loading.
|
||||
"""
|
||||
if Workflow.objects.exists():
|
||||
# If there is at least one workflow, then do nothing.
|
||||
@ -22,14 +22,17 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
state_1_2 = State.objects.create(name=ugettext_noop('accepted'),
|
||||
workflow=workflow_1,
|
||||
action_word='Accept',
|
||||
recommendation_label='Acceptance',
|
||||
css_class='success')
|
||||
state_1_3 = State.objects.create(name=ugettext_noop('rejected'),
|
||||
workflow=workflow_1,
|
||||
action_word='Reject',
|
||||
recommendation_label='Rejection',
|
||||
css_class='danger')
|
||||
state_1_4 = State.objects.create(name=ugettext_noop('not decided'),
|
||||
workflow=workflow_1,
|
||||
action_word='Do not decide',
|
||||
recommendation_label='No decision',
|
||||
css_class='default')
|
||||
state_1_1.next_states.add(state_1_2, state_1_3, state_1_4)
|
||||
workflow_1.first_state = state_1_1
|
||||
@ -44,6 +47,7 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
state_2_2 = State.objects.create(name=ugettext_noop('permitted'),
|
||||
workflow=workflow_2,
|
||||
action_word='Permit',
|
||||
recommendation_label='Permission',
|
||||
allow_create_poll=True,
|
||||
allow_submitter_edit=True,
|
||||
versioning=True,
|
||||
@ -51,11 +55,13 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
state_2_3 = State.objects.create(name=ugettext_noop('accepted'),
|
||||
workflow=workflow_2,
|
||||
action_word='Accept',
|
||||
recommendation_label='Acceptance',
|
||||
versioning=True,
|
||||
css_class='success')
|
||||
state_2_4 = State.objects.create(name=ugettext_noop('rejected'),
|
||||
workflow=workflow_2,
|
||||
action_word='Reject',
|
||||
recommendation_label='Rejection',
|
||||
versioning=True,
|
||||
css_class='danger')
|
||||
state_2_5 = State.objects.create(name=ugettext_noop('withdrawed'),
|
||||
@ -66,16 +72,19 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
state_2_6 = State.objects.create(name=ugettext_noop('adjourned'),
|
||||
workflow=workflow_2,
|
||||
action_word='Adjourn',
|
||||
recommendation_label='Adjournment',
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_7 = State.objects.create(name=ugettext_noop('not concerned'),
|
||||
workflow=workflow_2,
|
||||
action_word='Do not concern',
|
||||
recommendation_label='No concernment',
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_8 = State.objects.create(name=ugettext_noop('commited a bill'),
|
||||
state_2_8 = State.objects.create(name=ugettext_noop('refered to committee'),
|
||||
workflow=workflow_2,
|
||||
action_word='Commit a bill',
|
||||
action_word='Refer to committee',
|
||||
recommendation_label='Referral to committee',
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_9 = State.objects.create(name=ugettext_noop('needs review'),
|
||||
@ -86,6 +95,7 @@ def create_builtin_workflows(sender, **kwargs):
|
||||
state_2_10 = State.objects.create(name=ugettext_noop('rejected (not authorized)'),
|
||||
workflow=workflow_2,
|
||||
action_word='Reject (not authorized)',
|
||||
recommendation_label='Rejection (not authorized)',
|
||||
versioning=True,
|
||||
css_class='default')
|
||||
state_2_1.next_states.add(state_2_2, state_2_5, state_2_10)
|
||||
|
@ -15,11 +15,25 @@ angular.module('OpenSlidesApp.motions', [
|
||||
name: 'motions/workflowstate',
|
||||
methods: {
|
||||
getNextStates: function () {
|
||||
// TODO: Use filter with params with operator 'in'.
|
||||
var states = [];
|
||||
_.forEach(this.next_states_id, function (stateId) {
|
||||
states.push(DS.get('motions/workflowstate', stateId));
|
||||
});
|
||||
return states;
|
||||
},
|
||||
getRecommendations: function () {
|
||||
var params = {
|
||||
where: {
|
||||
'workflow_id': {
|
||||
'==': this.workflow_id
|
||||
},
|
||||
'recommendation_label': {
|
||||
'!=': null
|
||||
}
|
||||
}
|
||||
};
|
||||
return DS.filter('motions/workflowstate', params);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -188,6 +202,7 @@ angular.module('OpenSlidesApp.motions', [
|
||||
* - unsupport
|
||||
* - change_state
|
||||
* - reset_state
|
||||
* - change_recommendation
|
||||
*
|
||||
* NOTE: If you update this function please also update the
|
||||
* 'get_allowed_actions' function on server side in motions/models.py.
|
||||
@ -236,6 +251,8 @@ angular.module('OpenSlidesApp.motions', [
|
||||
return operator.hasPerms('motions.can_manage');
|
||||
case 'reset_state':
|
||||
return operator.hasPerms('motions.can_manage');
|
||||
case 'change_recommendation':
|
||||
return operator.hasPerms('motions.can_manage');
|
||||
case 'can_manage':
|
||||
return operator.hasPerms('motions.can_manage');
|
||||
default:
|
||||
@ -279,10 +296,16 @@ angular.module('OpenSlidesApp.motions', [
|
||||
}
|
||||
},
|
||||
hasOne: {
|
||||
'motions/workflowstate': {
|
||||
localField: 'state',
|
||||
localKey: 'state_id',
|
||||
}
|
||||
'motions/workflowstate': [
|
||||
{
|
||||
localField: 'state',
|
||||
localKey: 'state_id',
|
||||
},
|
||||
{
|
||||
localField: 'recommendation',
|
||||
localKey: 'recommendation_id',
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -330,31 +353,41 @@ angular.module('OpenSlidesApp.motions', [
|
||||
gettext('submitted');
|
||||
gettext('accepted');
|
||||
gettext('Accept');
|
||||
gettext('Acceptance');
|
||||
gettext('rejected');
|
||||
gettext('Reject');
|
||||
gettext('Rejection');
|
||||
gettext('not decided');
|
||||
gettext('Do not decide');
|
||||
gettext('No decision');
|
||||
// workflow 2
|
||||
gettext('Complex Workflow');
|
||||
gettext('published');
|
||||
gettext('permitted');
|
||||
gettext('Permit');
|
||||
gettext('Permission');
|
||||
gettext('accepted');
|
||||
gettext('Accept');
|
||||
gettext('Acceptance');
|
||||
gettext('rejected');
|
||||
gettext('Reject');
|
||||
gettext('Rejection');
|
||||
gettext('withdrawed');
|
||||
gettext('Withdraw');
|
||||
gettext('adjourned');
|
||||
gettext('Adjourn');
|
||||
gettext('Adjournment');
|
||||
gettext('not concerned');
|
||||
gettext('Do not concern');
|
||||
gettext('commited a bill');
|
||||
gettext('Commit a bill');
|
||||
gettext('No concernment');
|
||||
gettext('refered to committee');
|
||||
gettext('Refer to committee');
|
||||
gettext('Referral to committee');
|
||||
gettext('needs review');
|
||||
gettext('Needs review');
|
||||
gettext('rejected (not authorized)');
|
||||
gettext('Reject (not authorized)');
|
||||
gettext('Rejection (not authorized)');
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -1152,9 +1152,17 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': state_id});
|
||||
};
|
||||
// reset state
|
||||
$scope.reset_state = function (state_id) {
|
||||
$scope.reset_state = function () {
|
||||
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {});
|
||||
};
|
||||
// update recommendation
|
||||
$scope.updateRecommendation = function (recommendation_id) {
|
||||
$http.put('/rest/motions/motion/' + motion.id + '/set_recommendation/', {'recommendation': recommendation_id});
|
||||
};
|
||||
// reset state
|
||||
$scope.resetRecommendation = function () {
|
||||
$http.put('/rest/motions/motion/' + motion.id + '/set_recommendation/', {});
|
||||
};
|
||||
// create poll
|
||||
$scope.create_poll = function () {
|
||||
$http.post('/rest/motions/motion/' + motion.id + '/create_poll/', {});
|
||||
@ -1916,6 +1924,8 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
|
||||
gettext('The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40');
|
||||
gettext('Stop submitting new motions by non-staff users');
|
||||
gettext('Allow to disable versioning');
|
||||
gettext('Name of recommendation committee');
|
||||
gettext('Use an empty value to disable the recommendation system.');
|
||||
|
||||
// subgroup Amendments
|
||||
gettext('Amendments');
|
||||
|
@ -117,9 +117,9 @@
|
||||
<ul uib-dropdown-menu aria-labelledby="state-dropdown">
|
||||
<li ng-repeat="state in motion.state.getNextStates()">
|
||||
<a href ng-click="updateState(state.id)">{{ state.action_word | translate }}</a>
|
||||
<li class="divider" ng-if="motion.state.getNextStates().length">
|
||||
<li>
|
||||
<a href ng-if="motion.isAllowed('reset_state')" ng-click="reset_state()">
|
||||
<li class="divider" ng-if="motion.state.getNextStates().length && motion.isAllowed('reset_state')">
|
||||
<li ng-if="motion.isAllowed('reset_state')">
|
||||
<a href ng-click="reset_state()">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<translate>Reset state</translate>
|
||||
</a>
|
||||
@ -130,6 +130,34 @@
|
||||
{{ motion.state.name | translate }}
|
||||
</div>
|
||||
|
||||
<!-- Recommendation -->
|
||||
<div ng-if="config('motions_recommendations_by') != ''">
|
||||
<h3 ng-if="!motion.isAllowed('change_recommendation')" class="heading" translate>Recommendation</h3>
|
||||
<div ng-if="motion.isAllowed('change_recommendation')" class="heading">
|
||||
<span uib-dropdown>
|
||||
<a href id="recommendation-dropdown" class="drop-down-name" uib-dropdown-toggle>
|
||||
<translate>Recommendation</translate>
|
||||
<i class="fa fa-cog"></i>
|
||||
</a>
|
||||
<ul uib-dropdown-menu aria-labelledby="recommendation-dropdown">
|
||||
<li ng-repeat="recommendation in motion.state.getRecommendations()">
|
||||
<a href ng-click="updateRecommendation(recommendation.id)">
|
||||
{{ recommendation.recommendation_label | translate }}
|
||||
</a>
|
||||
<li class="divider" ng-if="motion.state.getRecommendations().length && motion.recommendation">
|
||||
<li ng-if="motion.recommendation">
|
||||
<a href ng-click="resetRecommendation()">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<translate>Reset recommendation</translate>
|
||||
</a>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
<div class="label" ng-class="'label-'+motion.recommendation.css_class">
|
||||
{{ motion.recommendation.recommendation_label | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Origin -->
|
||||
<h3 ng-if="motion.origin" translate>Origin</h3>
|
||||
{{ motion.origin }}
|
||||
|
@ -24,7 +24,14 @@ from .access_permissions import (
|
||||
WorkflowAccessPermissions,
|
||||
)
|
||||
from .exceptions import WorkflowError
|
||||
from .models import Category, Motion, MotionPoll, MotionVersion, Workflow
|
||||
from .models import (
|
||||
Category,
|
||||
Motion,
|
||||
MotionPoll,
|
||||
MotionVersion,
|
||||
State,
|
||||
Workflow,
|
||||
)
|
||||
from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf
|
||||
from .serializers import MotionPollSerializer
|
||||
|
||||
@ -57,7 +64,7 @@ class MotionViewSet(ModelViewSet):
|
||||
self.request.user.has_perm('motions.can_create') and
|
||||
(not config['motions_stop_submitting'] or
|
||||
self.request.user.has_perm('motions.can_manage')))
|
||||
elif self.action in ('destroy', 'manage_version', 'set_state', 'create_poll'):
|
||||
elif self.action in ('destroy', 'manage_version', 'set_state', 'set_recommendation', 'create_poll'):
|
||||
result = (self.request.user.has_perm('motions.can_see') and
|
||||
self.request.user.has_perm('motions.can_manage'))
|
||||
elif self.action == 'support':
|
||||
@ -278,6 +285,46 @@ class MotionViewSet(ModelViewSet):
|
||||
person=request.user)
|
||||
return Response({'detail': message})
|
||||
|
||||
@detail_route(methods=['put'])
|
||||
def set_recommendation(self, request, pk=None):
|
||||
"""
|
||||
Special view endpoint to set a recommendation of a motion.
|
||||
|
||||
Send PUT {'recommendation': <state_id>} to set and just PUT {} to
|
||||
reset the recommendation. Only managers can use this view.
|
||||
"""
|
||||
# Retrieve motion and recommendation state.
|
||||
motion = self.get_object()
|
||||
recommendation_state = request.data.get('recommendation')
|
||||
|
||||
# Set or reset recommendation.
|
||||
if recommendation_state is not None:
|
||||
# Check data and set recommendation.
|
||||
try:
|
||||
recommendation_state_id = int(recommendation_state)
|
||||
except ValueError:
|
||||
raise ValidationError({'detail': _('Invalid data. Recommendation must be an integer.')})
|
||||
recommendable_states = State.objects.filter(workflow=motion.workflow, recommendation_label__isnull=False)
|
||||
if recommendation_state_id not in [item.id for item in recommendable_states]:
|
||||
raise ValidationError(
|
||||
{'detail': _('You can not set the recommendation to {recommendation_state_id}.').format(
|
||||
recommendation_state_id=recommendation_state_id)})
|
||||
motion.set_recommendation(recommendation_state_id)
|
||||
else:
|
||||
# Reset recommendation.
|
||||
motion.recommendation = None
|
||||
|
||||
# Save motion.
|
||||
motion.save(update_fields=['recommendation'])
|
||||
label = motion.recommendation.recommendation_label if motion.recommendation else 'None'
|
||||
message = _('The recommendation of the motion was set to %s.') % label
|
||||
|
||||
# Write the log message and initiate response.
|
||||
motion.write_log(
|
||||
message_list=[ugettext_noop('Recommendation set to'), ' ', label],
|
||||
person=request.user)
|
||||
return Response({'detail': message})
|
||||
|
||||
@detail_route(methods=['post'])
|
||||
def create_poll(self, request, pk=None):
|
||||
"""
|
||||
|
@ -379,6 +379,82 @@ class SetState(TestCase):
|
||||
self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.name, 'submitted')
|
||||
|
||||
|
||||
class SetRecommendation(TestCase):
|
||||
"""
|
||||
Tests setting a recommendation.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.client.login(username='admin', password='admin')
|
||||
self.motion = Motion(
|
||||
title='test_title_ahfooT5leilahcohJ2uz',
|
||||
text='test_text_enoogh7OhPoo6eohoCus')
|
||||
self.motion.save()
|
||||
self.state_id_accepted = 2 # This should be the id of the state 'accepted'.
|
||||
|
||||
def test_set_recommendation(self):
|
||||
response = self.client.put(
|
||||
reverse('motion-set-recommendation', args=[self.motion.pk]),
|
||||
{'recommendation': self.state_id_accepted})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, {'detail': 'The recommendation of the motion was set to Acceptance.'})
|
||||
self.assertEqual(Motion.objects.get(pk=self.motion.pk).recommendation.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-recommendation', args=[self.motion.pk]),
|
||||
{'recommendation': 'accepted'})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data, {'detail': 'Invalid data. Recommendation must be an integer.'})
|
||||
|
||||
def test_set_unknown_recommendation(self):
|
||||
invalid_state_id = 0
|
||||
response = self.client.put(
|
||||
reverse('motion-set-recommendation', args=[self.motion.pk]),
|
||||
{'recommendation': invalid_state_id})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data, {'detail': 'You can not set the recommendation to %d.' % invalid_state_id})
|
||||
|
||||
def test_set_invalid_recommendation(self):
|
||||
# This is a valid state id, but this state is not recommendable because it belongs to a different workflow.
|
||||
invalid_state_id = 6 # State 'permitted'
|
||||
response = self.client.put(
|
||||
reverse('motion-set-recommendation', args=[self.motion.pk]),
|
||||
{'recommendation': invalid_state_id})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data, {'detail': 'You can not set the recommendation to %d.' % invalid_state_id})
|
||||
|
||||
def test_set_invalid_recommendation_2(self):
|
||||
# This is a valid state id, but this state is not recommendable because it has not recommendation label
|
||||
invalid_state_id = 1 # State 'submitted'
|
||||
self.motion.set_state(self.state_id_accepted)
|
||||
self.motion.save()
|
||||
response = self.client.put(
|
||||
reverse('motion-set-recommendation', args=[self.motion.pk]),
|
||||
{'recommendation': invalid_state_id})
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(response.data, {'detail': 'You can not set the recommendation to %d.' % invalid_state_id})
|
||||
|
||||
def test_reset(self):
|
||||
self.motion.set_recommendation(self.state_id_accepted)
|
||||
self.motion.save()
|
||||
response = self.client.put(reverse('motion-set-recommendation', args=[self.motion.pk]))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, {'detail': 'The recommendation of the motion was set to None.'})
|
||||
self.assertTrue(Motion.objects.get(pk=self.motion.pk).recommendation is None)
|
||||
|
||||
def test_set_recommendation_to_current_state(self):
|
||||
self.motion.set_state(self.state_id_accepted)
|
||||
self.motion.save()
|
||||
response = self.client.put(
|
||||
reverse('motion-set-recommendation', args=[self.motion.pk]),
|
||||
{'recommendation': self.state_id_accepted})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, {'detail': 'The recommendation of the motion was set to Acceptance.'})
|
||||
self.assertEqual(Motion.objects.get(pk=self.motion.pk).recommendation.name, 'accepted')
|
||||
|
||||
|
||||
class CreateMotionPoll(TestCase):
|
||||
"""
|
||||
Tests creating polls of motions.
|
||||
|
Loading…
Reference in New Issue
Block a user