diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index 203c38b26..14a2e1191 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -177,12 +177,12 @@ angular.module('OpenSlidesApp.core.site', [ // Split up state name // 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 // - appName: patterns[0] (e.g. "motions") // - moduleNames: patterns without first element (e.g. ["motion", "detail", "update"]) - var appName = '' + var appName = ''; var moduleName = ''; var moduleNames = []; if (patterns.length > 0) { @@ -194,7 +194,7 @@ angular.module('OpenSlidesApp.core.site', [ // example: ["motionBlock", "detail"] -> ["motion-block", "detail"] for (var i = 0; i < moduleNames.length; i++) { moduleNames[i] = moduleNames[i].replace(/([a-z\d])([A-Z])/g, '$1-$2').toLowerCase(); - }; + } // use special templateUrl for create and update view // 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) { var result = []; angular.forEach(obj, function(val, key) { diff --git a/openslides/motions/models.py b/openslides/motions/models.py index 421786e31..fdde434ef 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -17,6 +17,7 @@ from openslides.poll.models import ( BaseVote, CollectDefaultVotesMixin, ) +from openslides.utils.autoupdate import inform_changed_data from openslides.utils.models import RESTModelMixin from openslides.utils.search import user_name_helper @@ -192,7 +193,7 @@ class Motion(RESTModelMixin, models.Model): return self.title # 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. @@ -225,14 +226,19 @@ class Motion(RESTModelMixin, models.Model): if not self.identifier and isinstance(self.identifier, str): 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: # Do not save the version data if only some motion fields are updated. + if not skip_autoupdate: + inform_changed_data(self) return if use_version is False: # We do not need to save the version. + if not skip_autoupdate: + inform_changed_data(self) return elif use_version is None: use_version = self.get_last_version() @@ -249,6 +255,8 @@ class Motion(RESTModelMixin, models.Model): if use_version.id is None: if not self.version_data_changed(use_version): # We do not need to save the version. + if not skip_autoupdate: + inform_changed_data(self) return version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0 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. 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 # version is saved in the database. # 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: # TODO: Don't call this if it was not a new 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): """ @@ -530,6 +544,13 @@ class Motion(RESTModelMixin, models.Model): recommendation = State.objects.get(pk=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): """ Return a simple title string for the agenda. @@ -624,7 +645,7 @@ class Motion(RESTModelMixin, models.Model): 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. @@ -633,7 +654,8 @@ class Motion(RESTModelMixin, models.Model): """ if person and not person.is_authenticated(): 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): """ diff --git a/openslides/motions/static/js/motions/motion-block.js b/openslides/motions/static/js/motions/motion-block.js index ea45af12a..adff7931e 100644 --- a/openslides/motions/static/js/motions/motion-block.js +++ b/openslides/motions/static/js/motions/motion-block.js @@ -127,6 +127,7 @@ angular.module('OpenSlidesApp.motions.motionBlock', []) .controller('MotionBlockDetailCtrl', [ '$scope', + '$http', 'ngDialog', 'Motion', 'MotionBlockForm', @@ -134,7 +135,7 @@ angular.module('OpenSlidesApp.motions.motionBlock', []) 'motionBlock', 'Projector', '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'); Motion.bindAll({}, $scope, 'motions'); $scope.$watch(function () { @@ -148,6 +149,15 @@ angular.module('OpenSlidesApp.motions.motionBlock', []) $scope.openDialog = function (topic) { 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 }; + }); + }; } ]) diff --git a/openslides/motions/static/templates/motions/motion-block-detail.html b/openslides/motions/static/templates/motions/motion-block-detail.html index 87d649fd0..8028ff409 100644 --- a/openslides/motions/static/templates/motions/motion-block-detail.html +++ b/openslides/motions/static/templates/motions/motion-block-detail.html @@ -26,12 +26,11 @@
- + ng-bootbox-confirm-action="followRecommendations()" translate> - Set state for each motion according to their recommendation + Follow recommendations for all motions
diff --git a/openslides/motions/views.py b/openslides/motions/views.py index b73835c06..ce77b21fb 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -9,8 +9,9 @@ from django.utils.translation import ugettext_noop from reportlab.platypus import SimpleDocTemplate from rest_framework import status -from openslides.core.config import config -from openslides.utils.rest_api import ( +from ..core.config import config +from ..utils.autoupdate import inform_changed_data +from ..utils.rest_api import ( DestroyModelMixin, GenericViewSet, ModelViewSet, @@ -19,8 +20,7 @@ from openslides.utils.rest_api import ( ValidationError, detail_route, ) -from openslides.utils.views import APIView, PDFView, SingleObjectMixin - +from ..utils.views import APIView, PDFView, SingleObjectMixin from .access_permissions import ( CategoryAccessPermissions, MotionAccessPermissions, @@ -467,13 +467,35 @@ class MotionBlockViewSet(ModelViewSet): result = self.get_access_permissions().check_permissions(self.request.user) elif self.action == 'metadata': 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 self.request.user.has_perm('motions.can_manage')) else: result = False 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): """ diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index e3c0e6a57..9f656668f 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -145,7 +145,7 @@ def inform_changed_data(instances, information=None): """ root_instances = set() if not isinstance(instances, Iterable): - # Make surce instance is an iterable + # Make sure instances is an iterable instances = (instances, ) for instance in instances: try: diff --git a/tests/integration/motions/test_viewset.py b/tests/integration/motions/test_viewset.py index d60c8dd92..c0d165392 100644 --- a/tests/integration/motions/test_viewset.py +++ b/tests/integration/motions/test_viewset.py @@ -7,7 +7,7 @@ from rest_framework.test import APIClient from openslides.core.config import config 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.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_2.pk).identifier, 'test_prefix_ahz6tho2mooH8 2') 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)