Added view to follow recommendations.

for all motions of a motion block
This commit is contained in:
Norman Jäckel 2016-10-14 21:48:02 +02:00 committed by Emanuel Schütze
parent 0270c31b32
commit 20f8875dcd
7 changed files with 116 additions and 22 deletions

View File

@ -177,12 +177,12 @@ angular.module('OpenSlidesApp.core.site', [
// Split up state name // Split up state name
// example: "motions.motion.detail.update" -> ['motions', 'motion', 'detail', 'update'] // example: "motions.motion.detail.update" -> ['motions', 'motion', 'detail', 'update']
var patterns = state.name.split('.') var patterns = state.name.split('.');
// set app and module name from state // set app and module name from state
// - appName: patterns[0] (e.g. "motions") // - appName: patterns[0] (e.g. "motions")
// - moduleNames: patterns without first element (e.g. ["motion", "detail", "update"]) // - moduleNames: patterns without first element (e.g. ["motion", "detail", "update"])
var appName = '' var appName = '';
var moduleName = ''; var moduleName = '';
var moduleNames = []; var moduleNames = [];
if (patterns.length > 0) { if (patterns.length > 0) {
@ -194,7 +194,7 @@ angular.module('OpenSlidesApp.core.site', [
// example: ["motionBlock", "detail"] -> ["motion-block", "detail"] // example: ["motionBlock", "detail"] -> ["motion-block", "detail"]
for (var i = 0; i < moduleNames.length; i++) { for (var i = 0; i < moduleNames.length; i++) {
moduleNames[i] = moduleNames[i].replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase(); moduleNames[i] = moduleNames[i].replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase();
}; }
// use special templateUrl for create and update view // use special templateUrl for create and update view
// example: ["motion", "detail", "update"] -> "motion-form" // example: ["motion", "detail", "update"] -> "motion-form"
@ -1436,7 +1436,11 @@ angular.module('OpenSlidesApp.core.site', [
} }
]) ])
.filter("toArray", function(){ .filter('toArray', function(){
/*
* Transforms an object to an array. Items of the array are the values of
* the object elements.
*/
return function(obj) { return function(obj) {
var result = []; var result = [];
angular.forEach(obj, function(val, key) { angular.forEach(obj, function(val, key) {

View File

@ -17,6 +17,7 @@ from openslides.poll.models import (
BaseVote, BaseVote,
CollectDefaultVotesMixin, CollectDefaultVotesMixin,
) )
from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.models import RESTModelMixin from openslides.utils.models import RESTModelMixin
from openslides.utils.search import user_name_helper from openslides.utils.search import user_name_helper
@ -192,7 +193,7 @@ class Motion(RESTModelMixin, models.Model):
return self.title return self.title
# TODO: Use transaction # TODO: Use transaction
def save(self, use_version=None, *args, **kwargs): def save(self, use_version=None, skip_autoupdate=False, *args, **kwargs):
""" """
Save the motion. Save the motion.
@ -225,14 +226,19 @@ class Motion(RESTModelMixin, models.Model):
if not self.identifier and isinstance(self.identifier, str): if not self.identifier and isinstance(self.identifier, str):
self.identifier = None self.identifier = None
super(Motion, self).save(*args, **kwargs) # Always skip autoupdate. Maybe we run it later in this method.
super(Motion, self).save(skip_autoupdate=True, *args, **kwargs)
if 'update_fields' in kwargs: if 'update_fields' in kwargs:
# Do not save the version data if only some motion fields are updated. # Do not save the version data if only some motion fields are updated.
if not skip_autoupdate:
inform_changed_data(self)
return return
if use_version is False: if use_version is False:
# We do not need to save the version. # We do not need to save the version.
if not skip_autoupdate:
inform_changed_data(self)
return return
elif use_version is None: elif use_version is None:
use_version = self.get_last_version() use_version = self.get_last_version()
@ -249,6 +255,8 @@ class Motion(RESTModelMixin, models.Model):
if use_version.id is None: if use_version.id is None:
if not self.version_data_changed(use_version): if not self.version_data_changed(use_version):
# We do not need to save the version. # We do not need to save the version.
if not skip_autoupdate:
inform_changed_data(self)
return return
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0 version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
use_version.version_number = version_number + 1 use_version.version_number = version_number + 1
@ -256,16 +264,22 @@ class Motion(RESTModelMixin, models.Model):
# Necessary line if the version was set before the motion got an id. # Necessary line if the version was set before the motion got an id.
use_version.motion = use_version.motion use_version.motion = use_version.motion
use_version.save() # Always skip autoupdate. Maybe we run it later in this method.
use_version.save(skip_autoupdate=True)
# Set the active version of this motion. This has to be done after the # Set the active version of this motion. This has to be done after the
# version is saved in the database. # version is saved in the database.
# TODO: Move parts of these last lines of code outside the save method # TODO: Move parts of these last lines of code outside the save method
# when other versions than the last ones should be edited later on. # when other versions than the last one should be edited later on.
if self.active_version is None or not self.state.leave_old_version_active: if self.active_version is None or not self.state.leave_old_version_active:
# TODO: Don't call this if it was not a new version # TODO: Don't call this if it was not a new version
self.active_version = use_version self.active_version = use_version
self.save(update_fields=['active_version']) # Always skip autoupdate. Maybe we run it later in this method.
self.save(update_fields=['active_version'], skip_autoupdate=True)
# Finally run autoupdate if it is not skipped by caller.
if not skip_autoupdate:
inform_changed_data(self)
def version_data_changed(self, version): def version_data_changed(self, version):
""" """
@ -530,6 +544,13 @@ class Motion(RESTModelMixin, models.Model):
recommendation = State.objects.get(pk=recommendation) recommendation = State.objects.get(pk=recommendation)
self.recommendation = recommendation self.recommendation = recommendation
def follow_recommendation(self):
"""
Set the state of this motion to its recommendation.
"""
if self.recommendation is not None:
self.set_state(self.recommendation)
def get_agenda_title(self): def get_agenda_title(self):
""" """
Return a simple title string for the agenda. Return a simple title string for the agenda.
@ -624,7 +645,7 @@ class Motion(RESTModelMixin, models.Model):
return actions return actions
def write_log(self, message_list, person=None): def write_log(self, message_list, person=None, skip_autoupdate=False):
""" """
Write a log message. Write a log message.
@ -633,7 +654,8 @@ class Motion(RESTModelMixin, models.Model):
""" """
if person and not person.is_authenticated(): if person and not person.is_authenticated():
person = None person = None
MotionLog.objects.create(motion=self, message_list=message_list, person=person) motion_log = MotionLog(motion=self, message_list=message_list, person=person)
motion_log.save(skip_autoupdate=skip_autoupdate)
def is_amendment(self): def is_amendment(self):
""" """

View File

@ -127,6 +127,7 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
.controller('MotionBlockDetailCtrl', [ .controller('MotionBlockDetailCtrl', [
'$scope', '$scope',
'$http',
'ngDialog', 'ngDialog',
'Motion', 'Motion',
'MotionBlockForm', 'MotionBlockForm',
@ -134,7 +135,7 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
'motionBlock', 'motionBlock',
'Projector', 'Projector',
'ProjectionDefault', 'ProjectionDefault',
function($scope, ngDialog, Motion, MotionBlockForm, MotionBlock, motionBlock, Projector, ProjectionDefault) { function($scope, $http, ngDialog, Motion, MotionBlockForm, MotionBlock, motionBlock, Projector, ProjectionDefault) {
MotionBlock.bindOne(motionBlock.id, $scope, 'motionBlock'); MotionBlock.bindOne(motionBlock.id, $scope, 'motionBlock');
Motion.bindAll({}, $scope, 'motions'); Motion.bindAll({}, $scope, 'motions');
$scope.$watch(function () { $scope.$watch(function () {
@ -148,6 +149,15 @@ angular.module('OpenSlidesApp.motions.motionBlock', [])
$scope.openDialog = function (topic) { $scope.openDialog = function (topic) {
ngDialog.open(MotionBlockForm.getDialog(motionBlock)); ngDialog.open(MotionBlockForm.getDialog(motionBlock));
}; };
$scope.followRecommendations = function () {
$http.post('/rest/motions/motion-block/' + motionBlock.id + '/follow_recommendations/')
.success(function(data) {
$scope.alert = { type: 'success', msg: data.detail, show: true };
})
.error(function(data) {
$scope.alert = { type: 'danger', msg: data.detail, show: true };
});
};
} }
]) ])

View File

@ -26,12 +26,11 @@
</div> </div>
<div class="details"> <div class="details">
<!-- set state button (TODO)-->
<a os-perms="motions.can_manage" class="btn btn-default btn" <a os-perms="motions.can_manage" class="btn btn-default btn"
ng-bootbox-confirm="{{ 'Are you sure you want to override the state of all motions of this motion block?' | translate }}" ng-bootbox-confirm="{{ 'Are you sure you want to override the state of all motions of this motion block?' | translate }}"
ng-bootbox-confirm-action="" translate> ng-bootbox-confirm-action="followRecommendations()" translate>
<i class="fa fa-magic fa-lg"></i> <i class="fa fa-magic fa-lg"></i>
<translate>Set state for each motion according to their recommendation</translate> <translate>Follow recommendations for all motions</translate>
</a> </a>
<div class="row spacer form-group"> <div class="row spacer form-group">

View File

@ -9,8 +9,9 @@ from django.utils.translation import ugettext_noop
from reportlab.platypus import SimpleDocTemplate from reportlab.platypus import SimpleDocTemplate
from rest_framework import status from rest_framework import status
from openslides.core.config import config from ..core.config import config
from openslides.utils.rest_api import ( from ..utils.autoupdate import inform_changed_data
from ..utils.rest_api import (
DestroyModelMixin, DestroyModelMixin,
GenericViewSet, GenericViewSet,
ModelViewSet, ModelViewSet,
@ -19,8 +20,7 @@ from openslides.utils.rest_api import (
ValidationError, ValidationError,
detail_route, detail_route,
) )
from openslides.utils.views import APIView, PDFView, SingleObjectMixin from ..utils.views import APIView, PDFView, SingleObjectMixin
from .access_permissions import ( from .access_permissions import (
CategoryAccessPermissions, CategoryAccessPermissions,
MotionAccessPermissions, MotionAccessPermissions,
@ -467,13 +467,35 @@ class MotionBlockViewSet(ModelViewSet):
result = self.get_access_permissions().check_permissions(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action == 'metadata': elif self.action == 'metadata':
result = self.request.user.has_perm('motions.can_see') result = self.request.user.has_perm('motions.can_see')
elif self.action in ('create', 'partial_update', 'update', 'destroy'): elif self.action in ('create', 'partial_update', 'update', 'destroy', 'follow_recommendations'):
result = (self.request.user.has_perm('motions.can_see') and result = (self.request.user.has_perm('motions.can_see') and
self.request.user.has_perm('motions.can_manage')) self.request.user.has_perm('motions.can_manage'))
else: else:
result = False result = False
return result return result
@detail_route(methods=['post'])
def follow_recommendations(self, request, pk=None):
"""
View to set the states of all motions of this motion block each to
its recommendation. It is a POST request without any data.
"""
motion_block = self.get_object()
instances = []
with transaction.atomic():
for motion in motion_block.motion_set.all():
# Follow recommendation.
motion.follow_recommendation()
motion.save(skip_autoupdate=True)
# Write the log message.
motion.write_log(
message_list=[ugettext_noop('State set to'), ' ', motion.state.name],
person=request.user,
skip_autoupdate=True)
instances.append(motion)
inform_changed_data(instances)
return Response({'detail': _('Followed recommendations successfully.')})
class WorkflowViewSet(ModelViewSet): class WorkflowViewSet(ModelViewSet):
""" """

View File

@ -145,7 +145,7 @@ def inform_changed_data(instances, information=None):
""" """
root_instances = set() root_instances = set()
if not isinstance(instances, Iterable): if not isinstance(instances, Iterable):
# Make surce instance is an iterable # Make sure instances is an iterable
instances = (instances, ) instances = (instances, )
for instance in instances: for instance in instances:
try: try:

View File

@ -7,7 +7,7 @@ from rest_framework.test import APIClient
from openslides.core.config import config from openslides.core.config import config
from openslides.core.models import Tag from openslides.core.models import Tag
from openslides.motions.models import Category, Motion, State from openslides.motions.models import Category, Motion, MotionBlock, State
from openslides.users.models import User from openslides.users.models import User
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -726,3 +726,40 @@ class NumberMotionsInCategory(TestCase):
self.assertEqual(Motion.objects.get(pk=self.motion.pk).identifier, None) self.assertEqual(Motion.objects.get(pk=self.motion.pk).identifier, None)
self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2') self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2')
self.assertEqual(Motion.objects.get(pk=self.motion_3.pk).identifier, 'test_prefix_ahz6tho2mooH8 1') self.assertEqual(Motion.objects.get(pk=self.motion_3.pk).identifier, 'test_prefix_ahz6tho2mooH8 1')
class FollowRecommendationsForMotionBlock(TestCase):
"""
Tests following the recommendations of motions in an motion block.
"""
def setUp(self):
self.state_id_accepted = 2 # This should be the id of the state 'accepted'.
self.state_id_rejected = 3 # This should be the id of the state 'rejected'.
self.client = APIClient()
self.client.login(username='admin', password='admin')
self.motion_block = MotionBlock.objects.create(
title='test_motion_block_name_Ufoopiub7quaezaepeic')
self.motion = Motion(
title='test_title_yo8ohy5eifeiyied2AeD',
text='test_text_chi1aeth5faPhueQu8oh',
motion_block=self.motion_block)
self.motion.save()
self.motion.set_recommendation(self.state_id_accepted)
self.motion.save()
self.motion_2 = Motion(
title='test_title_eith0EemaW8ahZa9Piej',
text='test_text_haeho1ohk3ou7pau2Jee',
motion_block=self.motion_block)
self.motion_2.save()
self.motion_2.set_recommendation(self.state_id_rejected)
self.motion_2.save()
def test_follow_recommendations_for_motion_block(self):
response = self.client.post(reverse('motionblock-follow-recommendations', args=[self.motion_block.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(Motion.objects.get(pk=self.motion.pk).state.id, self.state_id_accepted)
self.assertEqual(Motion.objects.get(pk=self.motion_2.pk).state.id, self.state_id_rejected)