From cf29f976130e1cd1d0ffab7c3c1a8df753c95db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sat, 11 May 2019 22:43:32 +0200 Subject: [PATCH] Added view for bulk category set and bulk motion block set. Due to rebasing this PR, this are the appropriate authorships: Server: @normanjaeckel and @FinnStutzenstein Client: @MaximilianKrambach --- CHANGELOG.rst | 6 +- .../motions/motion-repository.service.ts | 28 +++ .../services/motion-multiselect.service.ts | 39 ++--- openslides/motions/views.py | 162 ++++++++++++++++++ 4 files changed, 206 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5343b65ab..14967397b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,10 +40,10 @@ Motions: [#4235, #4518, #4521]. - Allowed submitters to set state of new motions in complex and customized workflow [#4236]. - - Added multi select action to manage submitters, tags, states and - recommendations [#4037, #4132]. + - Added multi select action to manage submitters, categories, motion blocks, + tags, states and recommendations [#4037, #4132, #4702]. - Added timestampes for motions [#4134]. - - New config option to set reason as required field [#4232] + - New config option to set reason as required field [#4232]. User: - Added new admin group which grants all permissions. Users of existing group diff --git a/client/src/app/core/repositories/motions/motion-repository.service.ts b/client/src/app/core/repositories/motions/motion-repository.service.ts index 4f0059960..682a952a7 100644 --- a/client/src/app/core/repositories/motions/motion-repository.service.ts +++ b/client/src/app/core/repositories/motions/motion-repository.service.ts @@ -350,6 +350,34 @@ export class MotionRepositoryService extends BaseAgendaContentObjectRepository { + const restPath = `/rest/motions/motion/manage_multiple_motion_block/`; + const motionsIdMap: { id: number; motion_block: number }[] = viewMotions.map(motion => { + return { id: motion.id, motion_block: motionblockId }; + }); + await this.httpService.post(restPath, { motions: motionsIdMap }); + } + + /** + * Set the category of motions in bulk + * + * @param viewMotion target motion + * @param categoryId the number that indicates the category + */ + public async setMultiCategory(viewMotions: ViewMotion[], categoryId: number): Promise { + const restPath = `/rest/motions/motion/manage_multiple_category/`; + const motionsIdMap: { id: number; category: number }[] = viewMotions.map(motion => { + return { id: motion.id, category: categoryId }; + }); + await this.httpService.post(restPath, { motions: motionsIdMap }); + } + /** * Set the recommenders state of a motion * diff --git a/client/src/app/site/motions/services/motion-multiselect.service.ts b/client/src/app/site/motions/services/motion-multiselect.service.ts index b48d46fee..68343896f 100644 --- a/client/src/app/site/motions/services/motion-multiselect.service.ts +++ b/client/src/app/site/motions/services/motion-multiselect.service.ts @@ -179,20 +179,12 @@ export class MotionMultiselectService { clearChoice ); if (selectedChoice) { - let i = 0; - for (const motion of motions) { - ++i; - const message = - this.translate.instant(this.messageForSpinner) + - `\n${i} ` + - this.translate.instant('of') + - ` ${motions.length}`; - this.spinnerService.setVisibility(true, message); - await this.repo.update( - { category_id: selectedChoice.action ? null : (selectedChoice.items as number) }, - motion - ); - } + const message = this.translate.instant(this.messageForSpinner); + this.spinnerService.setVisibility(true, message); + await this.repo.setMultiCategory(motions, selectedChoice.items as number).catch(error => { + this.spinnerService.setVisibility(false); + throw error; + }); this.spinnerService.setVisibility(false); } } @@ -315,18 +307,13 @@ export class MotionMultiselectService { clearChoice ); if (selectedChoice) { - let i = 0; - for (const motion of motions) { - ++i; - const message = - this.translate.instant(this.messageForSpinner) + - `\n${i} ` + - this.translate.instant('of') + - ` ${motions.length}`; - this.spinnerService.setVisibility(true, message); - const blockId = selectedChoice.action ? null : (selectedChoice.items as number); - await this.repo.update({ motion_block_id: blockId }, motion); - } + const message = this.translate.instant(this.messageForSpinner); + this.spinnerService.setVisibility(true, message); + const blockId = selectedChoice.action ? null : (selectedChoice.items as number); + await this.repo.setMultiMotionBlock(motions, blockId).catch(error => { + this.spinnerService.setVisibility(false); + throw error; + }); this.spinnerService.setVisibility(false); } } diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 6fbaf07d1..361f0904e 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -80,6 +80,8 @@ class MotionViewSet(TreeSortMixin, ModelViewSet): result = has_perm(self.request.user, "motions.can_see") # The rest of the check is done in the respective method. See below. elif self.action in ( + "manage_multiple_category", + "manage_multiple_motion_block", "manage_multiple_state", "set_recommendation", "manage_multiple_recommendation", @@ -523,6 +525,166 @@ class MotionViewSet(TreeSortMixin, ModelViewSet): # Initiate response. return Response({"detail": message}) + @list_route(methods=["post"]) + @transaction.atomic + def manage_multiple_category(self, request): + """ + Set categories of multiple motions. + + Send POST {"motions": [... see schema ...]} to changed the categories. + """ + motions = request.data.get("motions") + + schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Motion manage multiple categories schema", + "description": "An array of motion ids with the respective category id that should be set as category.", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"description": "The id of the motion.", "type": "integer"}, + "category": { + "description": "The id for the category that should become the new category.", + "anyOf": [{"type": "number", "minimum": 1}, {"type": "null"}], + }, + }, + "required": ["id", "category"], + }, + "uniqueItems": True, + } + + # Validate request data. + try: + jsonschema.validate(motions, schema) + except jsonschema.ValidationError as err: + raise ValidationError({"detail": str(err)}) + + motion_result = [] + for item in motions: + # Get motion. + try: + motion = Motion.objects.get(pk=item["id"]) + except Motion.DoesNotExist: + raise ValidationError({"detail": f"Motion {item['id']} does not exist"}) + + # Get category + category = None + if item["category"] is not None: + try: + category = Category.objects.get(pk=item["category"]) + except Category.DoesNotExist: + raise ValidationError( + {"detail": f"Category {item['category']} does not exist"} + ) + + # Set category + motion.category = category + + # Save motion. + motion.save( + update_fields=["category", "last_modified"], skip_autoupdate=True + ) + + # Fire autoupdate again to save information to OpenSlides history. + information = ( + ["Category removed"] + if category is None + else ["Category set to {arg1}", category.name] + ) + inform_changed_data( + motion, information=information, user_id=request.user.pk + ) + + # Finish motion. + motion_result.append(motion) + + # Send response. + return Response( + {"detail": f"Category of {len(motion_result)} motions successfully set."} + ) + + @list_route(methods=["post"]) + @transaction.atomic + def manage_multiple_motion_block(self, request): + """ + Set motion blocks of multiple motions. + + Send POST {"motions": [... see schema ...]} to changed the motion blocks. + """ + motions = request.data.get("motions") + + schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Motion manage multiple motion blocks schema", + "description": "An array of motion ids with the respective motion block id that should be set as motion block.", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"description": "The id of the motion.", "type": "integer"}, + "motion_block": { + "description": "The id for the motion block that should become the new motion block.", + "anyOf": [{"type": "number", "minimum": 1}, {"type": "null"}], + }, + }, + "required": ["id", "motion_block"], + }, + "uniqueItems": True, + } + + # Validate request data. + try: + jsonschema.validate(motions, schema) + except jsonschema.ValidationError as err: + raise ValidationError({"detail": str(err)}) + + motion_result = [] + for item in motions: + # Get motion. + try: + motion = Motion.objects.get(pk=item["id"]) + except Motion.DoesNotExist: + raise ValidationError({"detail": f"Motion {item['id']} does not exist"}) + + # Get motion block + motion_block = None + if item["motion_block"] is not None: + try: + motion_block = MotionBlock.objects.get(pk=item["motion_block"]) + except MotionBlock.DoesNotExist: + raise ValidationError( + {"detail": f"MotionBlock {item['motion_block']} does not exist"} + ) + + # Set motion bock + motion.motion_block = motion_block + + # Save motion. + motion.save( + update_fields=["motion_block", "last_modified"], skip_autoupdate=True + ) + + # Fire autoupdate again to save information to OpenSlides history. + information = ( + ["Motion block removed"] + if motion_block is None + else ["Motion block set to {arg1}", motion_block.title] + ) + inform_changed_data( + motion, information=information, user_id=request.user.pk + ) + + # Finish motion. + motion_result.append(motion) + + # Send response. + return Response( + { + "detail": f"Motion block of {len(motion_result)} motions successfully set." + } + ) + @detail_route(methods=["put"]) def set_state(self, request, pk=None): """