From 10038b782f979df64dcc11553adc03f4901204e8 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Tue, 17 Apr 2018 10:05:27 +0200 Subject: [PATCH] One request for each projection. Added some validation for clear_elements and prune_elements --- CHANGELOG | 1 + openslides/agenda/static/js/agenda/base.js | 39 ++++--- .../assignments/static/js/assignments/base.js | 19 +-- openslides/core/static/js/core/base.js | 17 +-- openslides/core/views.py | 108 +++++++++++++++--- openslides/motions/static/js/motions/base.js | 28 ++--- openslides/utils/main.py | 4 +- 7 files changed, 150 insertions(+), 66 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 843f1f5cb..8cb315ea1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -132,6 +132,7 @@ Core: - Reset scroll level for each new projection [#3686]. - Scroll to top on every state change [#3689]. - Added pagination on top of lists [#3698]. +- New api route to project items with just one request needed [#3713]. Mediafiles: - New form for uploading multiple files [#3650]. diff --git a/openslides/agenda/static/js/agenda/base.js b/openslides/agenda/static/js/agenda/base.js index f5a002e59..0e1136a54 100644 --- a/openslides/agenda/static/js/agenda/base.js +++ b/openslides/agenda/static/js/agenda/base.js @@ -129,23 +129,24 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users']) project: function (projectorId, tree) { if (tree) { var isProjectedIds = this.isProjected(tree); - _.forEach(isProjectedIds, function (id) { - $http.post('/rest/core/projector/' + id + '/clear_elements/'); - }); + var requestData = { + clear_ids: isProjectedIds, + }; // Activate, if the projector_id is a new projector. if (_.indexOf(isProjectedIds, projectorId) == -1) { - return $http.post( - '/rest/core/projector/' + projectorId + '/prune_elements/', - [{ + requestData.prune = { + id: projectorId, + element: { name: 'agenda/item-list', tree: true, - id: this.id - }] - ); + id: this.id, + }, + }; } + return $http.post('/rest/core/projector/project/', requestData); } else { // Project the content object var contentObject = DS.get(this.content_object.collection, this.content_object.id); - contentObject.project(projectorId); + return contentObject.project(projectorId); } }, // override isProjected function of jsDataModel factory @@ -185,15 +186,19 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users']) // project list of speakers projectListOfSpeakers: function(projectorId) { var isProjectedIds = this.isListOfSpeakersProjected(); - _.forEach(isProjectedIds, function (id) { - $http.post('/rest/core/projector/' + id + '/clear_elements/'); - }); + var requestData = { + clear_ids: isProjectedIds, + }; if (_.indexOf(isProjectedIds, projectorId) == -1) { - return $http.post( - '/rest/core/projector/' + projectorId + '/prune_elements/', - [{name: 'agenda/list-of-speakers', id: this.id}] - ); + requestData.prune = { + id: projectorId, + element: { + name: 'agenda/list-of-speakers', + id: this.id, + }, + }; } + return $http.post('/rest/core/projector/project/', requestData); }, // check if list of speakers is projected isListOfSpeakersProjected: function () { diff --git a/openslides/assignments/static/js/assignments/base.js b/openslides/assignments/static/js/assignments/base.js index b92c0e100..203a7d9b2 100644 --- a/openslides/assignments/static/js/assignments/base.js +++ b/openslides/assignments/static/js/assignments/base.js @@ -356,15 +356,20 @@ angular.module('OpenSlidesApp.assignments', []) // override project function of jsDataModel factory project: function (projectorId, pollId) { var isProjectedIds = this.isProjected(pollId); - _.forEach(isProjectedIds, function (id) { - $http.post('/rest/core/projector/' + id + '/clear_elements/'); - }); + var requestData = { + clear_ids: isProjectedIds, + }; if (_.indexOf(isProjectedIds, projectorId) == -1) { - return $http.post( - '/rest/core/projector/' + projectorId + '/prune_elements/', - [{name: 'assignments/assignment', id: this.id, poll: pollId}] - ); + requestData.prune = { + id: projectorId, + element: { + name: 'assignments/assignment', + id: this.id, + poll: pollId + }, + }; } + return $http.post('/rest/core/projector/project/', requestData); }, // override isProjected function of jsDataModel factory isProjected: function (poll_id, anyPoll) { diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index 5732f7a92..8bd7ffbad 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -733,16 +733,17 @@ angular.module('OpenSlidesApp.core', [ BaseModel.prototype.project = function(projectorId) { // if this object is already projected on projectorId, delete this element from this projector var isProjectedIds = this.isProjected(); - _.forEach(isProjectedIds, function (id) { - $http.post('/rest/core/projector/' + id + '/clear_elements/'); - }); + var requestData = { + clear_ids: isProjectedIds, + }; // Show the element, if it was not projected before on the given projector if (_.indexOf(isProjectedIds, projectorId) == -1) { - return $http.post( - '/rest/core/projector/' + projectorId + '/prune_elements/', - [{name: this.getResourceName(), id: this.id}] - ); + requestData.prune = { + id: projectorId, + element: {name: this.getResourceName(), id: this.id}, + }; } + return $http.post('/rest/core/projector/project/', requestData); }; BaseModel.prototype.isProjected = function() { // Returns the ids of all projectors if there is a projector element @@ -761,7 +762,7 @@ angular.module('OpenSlidesApp.core', [ }); return isProjectedIds; }; - // Override this method to get object spzific behavior + // Override this method to get object specific behavior BaseModel.prototype.isRelatedProjected = function() { throw "needs to be implemented!"; }; diff --git a/openslides/core/views.py b/openslides/core/views.py index f23974e80..2c81ff7c7 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -213,8 +213,8 @@ class ProjectorViewSet(ModelViewSet): elif self.action in ( 'create', 'update', 'partial_update', 'destroy', 'activate_elements', 'prune_elements', 'update_elements', 'deactivate_elements', 'clear_elements', - 'control_view', 'set_resolution', 'set_scroll', 'control_blank', 'broadcast', - 'set_projectiondefault', + 'project', 'control_view', 'set_resolution', 'set_scroll', 'control_blank', + 'broadcast', 'set_projectiondefault', ): result = (has_perm(self.request.user, 'core.can_see_projector') and has_perm(self.request.user, 'core.can_manage_projector')) @@ -271,24 +271,39 @@ class ProjectorViewSet(ModelViewSet): if not isinstance(request.data, list): raise ValidationError({'detail': 'Data must be a list.'}) - projector_instance = self.get_object() - # reset scroll level - if (projector_instance.scroll != 0): - projector_instance.scroll = 0 - projector_instance.save() - projector_config = {} - for key, value in projector_instance.config.items(): - if value.get('stable'): - projector_config[key] = value - for element in request.data: + projector = self.get_object() + elements = request.data + if not isinstance(elements, list): + raise ValidationError({'detail': _('The data has to be a list.')}) + for element in elements: + if not isinstance(element, dict): + raise ValidationError({'detail': _('All elements have to be dicts.')}) if element.get('name') is None: raise ValidationError({'detail': 'Invalid projector element. Name is missing.'}) + return Response(self.prune(projector, elements)) + + def prune(self, projector, elements): + """ + Prunes all non stable elements from the projector and adds the given elements. + The elements have to a list of dicts, each gict containing at least a name. This + is not validated at this point! Should be done before. + Returns the new serialized data. + """ + # reset scroll level + if (projector.scroll != 0): + projector.scroll = 0 + projector.save() + projector_config = {} + for key, value in projector.config.items(): + if value.get('stable'): + projector_config[key] = value + for element in elements: projector_config[uuid.uuid4().hex] = element - serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) + serializer = self.get_serializer(projector, data={'config': projector_config}, partial=False) serializer.is_valid(raise_exception=True) serializer.save() - return Response(serializer.data) + return serializer.data @detail_route(methods=['post']) def update_elements(self, request, pk): @@ -370,16 +385,73 @@ class ProjectorViewSet(ModelViewSet): entries with stable == True. It expects a POST request to /rest/core/projector//clear_elements/. """ - projector_instance = self.get_object() + projector = self.get_object() + return Response(self.clear(projector)) + + def clear(self, projector): projector_config = {} - for key, value in projector_instance.config.items(): + for key, value in projector.config.items(): if value.get('stable'): projector_config[key] = value - serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False) + serializer = self.get_serializer(projector, data={'config': projector_config}, partial=False) serializer.is_valid(raise_exception=True) serializer.save() - return Response(serializer.data) + return serializer.data + + @list_route(methods=['post']) + def project(self, request, *args, **kwargs): + """ + REST API operation. Does a combination of clear_elements and prune_elements: + In the most cases when projecting an element it first need to be removed from + all projectors where it is projected. In a second step the new element (which + may be not given if the element is just deprojected) needs to be projected on + a maybe different projector. The request data has to have this scheme: + { + clear_ids: [, ...], # May be an empty list + prune: { # May not be given. + id: , + element: + } + } + """ + # Get projector ids to clear + clear_projector_ids = request.data.get('clear_ids', []) + for id in clear_projector_ids: + if not isinstance(id, int): + raise ValidationError({'detail': _('The id "{}" has to be int.').format(id)}) + + # Get the projector id and validate element to prune. This is optional. + prune = request.data.get('prune') + if prune is not None: + if not isinstance(prune, dict): + raise ValidationError({'detail': _('Prune has to be an object.')}) + prune_projector_id = prune.get('id') + if not isinstance(prune_projector_id, int): + raise ValidationError({'detail': _('The prune projector id has to be int.')}) + + # Get the projector after all clear operations, but check, if it exist. + if not Projector.objects.filter(pk=prune_projector_id).exists(): + raise ValidationError({ + 'detail': _('The projector with id "{}" does not exist').format(prune_projector_id)}) + + prune_element = prune.get('element', {}) + if not isinstance(prune_element, dict): + raise ValidationError({'detail': _('Prune element has to be a dict or not given.')}) + if prune_element.get('name') is None: + raise ValidationError({'detail': 'Invalid projector element. Name is missing.'}) + + # First step: Clear all given projectors + for projector in Projector.objects.filter(pk__in=clear_projector_ids): + self.clear(projector) + + # Second step: optionally prune + if prune is not None: + # This get is save. We checked that the projector exists above. + prune_projector = Projector.objects.get(pk=prune_projector_id) + self.prune(prune_projector, [prune_element]) + + return Response() @detail_route(methods=['post']) def set_resolution(self, request, pk): diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js index a874aa630..90d3191bf 100644 --- a/openslides/motions/static/js/motions/base.js +++ b/openslides/motions/static/js/motions/base.js @@ -44,12 +44,10 @@ angular.module('OpenSlidesApp.motions', [ .factory('Workflow', [ 'DS', - 'jsDataModel', 'WorkflowState', - function (DS, jsDataModel, WorkflowState) { + function (DS, WorkflowState) { return DS.defineResource({ name: 'motions/workflow', - useClass: jsDataModel, relations: { hasMany: { 'motions/workflowstate': { @@ -652,13 +650,12 @@ angular.module('OpenSlidesApp.motions', [ * Also sets the projection mode if given; If not it projects in 'original' mode. */ project: function (projectorId, mode) { // if this object is already projected on projectorId, delete this element from this projector - var isProjected = this.isProjectedWithMode(); - _.forEach(isProjected, function (mapping) { - $http.post('/rest/core/projector/' + mapping.projectorId + '/clear_elements/'); - }); + var requestData = { + clear_ids: this.isProjected(), + }; // Was there a projector with the same id and mode as the given id and mode? // If not, project the motion. - var wasProjectedBefore = _.some(isProjected, function (mapping) { + var wasProjectedBefore = _.some(this.isProjectedWithMode(), function (mapping) { var value = (mapping.projectorId === projectorId); if (mode) { value = value && (mapping.mode === mode); @@ -667,13 +664,16 @@ angular.module('OpenSlidesApp.motions', [ }); mode = mode || Config.get('motions_recommendation_text_mode').value; if (!wasProjectedBefore) { - return $http.post( - '/rest/core/projector/' + projectorId + '/prune_elements/', - [{name: name, - id: this.id, - mode: mode}] - ); + requestData.prune = { + id: projectorId, + element: { + name: name, + id: this.id, + mode: mode, + }, + }; } + return $http.post('/rest/core/projector/project/', requestData); }, isProjected: function (mode) { var self = this; diff --git a/openslides/utils/main.py b/openslides/utils/main.py index f41cf2ff0..e89bd40fe 100644 --- a/openslides/utils/main.py +++ b/openslides/utils/main.py @@ -181,7 +181,7 @@ def get_win32_app_data_dir() -> str: """ Returns the directory of Windows' AppData directory. """ - shell32 = ctypes.WinDLL("shell32.dll") + shell32 = ctypes.WinDLL('shell32.dll') # type: ignore SHGetFolderPath = shell32.SHGetFolderPathW SHGetFolderPath.argtypes = ( ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint32, @@ -197,7 +197,7 @@ def get_win32_app_data_dir() -> str: # TODO: Write other exception raise Exception("Could not determine Windows' APPDATA path") - return buf.value + return buf.value.decode('utf-8') def get_win32_portable_dir() -> str: