From 658b1a360d4b9eccdf59b800beaecc71969f044f Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Tue, 21 May 2019 13:52:10 +0200 Subject: [PATCH] Internal flag for motion blocks - ServerSide - Adds the 'internal'-flag to the edit view of motion blocks --- .../core/core-services/operator.service.ts | 2 +- .../app/shared/models/motions/motion-block.ts | 1 + .../site/motions/models/view-motion-block.ts | 4 ++ .../motion-block-detail.component.html | 52 ++++++++++------ .../motion-block-detail.component.scss | 4 ++ .../motion-block-detail.component.ts | 51 ++++++++++------ .../motion-block-list.component.html | 14 ++++- .../motion-block-list.component.ts | 30 ++++++---- openslides/motions/access_permissions.py | 14 +++++ .../migrations/0027_motion_block_internal.py | 16 +++++ openslides/motions/models.py | 6 ++ openslides/motions/serializers.py | 1 + tests/integration/motions/test_viewset.py | 60 +++++++++++++++++++ 13 files changed, 202 insertions(+), 53 deletions(-) create mode 100644 openslides/motions/migrations/0027_motion_block_internal.py diff --git a/client/src/app/core/core-services/operator.service.ts b/client/src/app/core/core-services/operator.service.ts index 7d72004c2..44e3ddc13 100644 --- a/client/src/app/core/core-services/operator.service.ts +++ b/client/src/app/core/core-services/operator.service.ts @@ -342,7 +342,7 @@ export class OperatorService implements OnAfterAppsLoaded { public isInGroupIds(...groupIds: number[]): boolean { if (!this.isInGroupIdsNonAdminCheck(...groupIds)) { // An admin has all perms and is technically in every group. - return this.user.groups_id.includes(2); + return this.user && this.user.groups_id.includes(2); } return true; } diff --git a/client/src/app/shared/models/motions/motion-block.ts b/client/src/app/shared/models/motions/motion-block.ts index 954d89d77..d70d55684 100644 --- a/client/src/app/shared/models/motions/motion-block.ts +++ b/client/src/app/shared/models/motions/motion-block.ts @@ -9,6 +9,7 @@ export class MotionBlock extends BaseModelWithAgendaItemAndListOfSpeakers
-

{{ block.title }}

- -
- - - -
+

+ lock + {{ block.title }} +

@@ -23,10 +13,6 @@ more_vert - -
- -
@@ -111,7 +97,7 @@
@@ -121,3 +107,31 @@
+ + +

+ {{ 'Edit details for' | translate }} {{ block.title }} +

+
+
+ + + + Internal +
+
+
+ + +
+
diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.scss b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.scss index d06686661..2f5db7e35 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.scss +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.scss @@ -51,3 +51,7 @@ justify-content: flex-end !important; } } + +.edit-form { + overflow: hidden; +} diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts index edc7769e4..0744edb65 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts @@ -1,7 +1,7 @@ import { ActivatedRoute, Router } from '@angular/router'; -import { Component, OnInit, ViewChild } from '@angular/core'; -import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { MatSnackBar } from '@angular/material'; +import { Component, OnInit, ViewChild, TemplateRef } from '@angular/core'; +import { FormGroup, Validators, FormBuilder } from '@angular/forms'; +import { MatSnackBar, MatDialog } from '@angular/material'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; @@ -32,17 +32,18 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent; + /** * Constructor for motion block details * @@ -66,7 +67,9 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent { @@ -171,7 +170,7 @@ export class MotionBlockDetailComponent extends ListViewBaseComponent this.dialog.closeAll()) + .catch(this.raiseError); } /** * Click handler for the edit button */ public toggleEditMode(): void { - this.editBlock = !this.editBlock; + this.blockEditForm = this.fb.group({ + title: [this.block.title, Validators.required], + internal: [this.block.internal] + }); + + const dialogRef = this.dialog.open(this.editDialog, { + width: '400px', + maxWidth: '90vw', + maxHeight: '90vh', + disableClose: true + }); + + dialogRef.keydownEvents().subscribe((event: KeyboardEvent) => { + if (event.key === 'Enter' && event.shiftKey) { + this.saveBlock(); + } + }); } /** diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html index 69b7f960b..66ed79089 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.html @@ -4,7 +4,7 @@ - + New motion block
@@ -18,6 +18,11 @@

+ +

+ Internal +

+

- + @@ -65,7 +70,10 @@ Title - {{ block.title }} + + lock + {{ block.title }} + diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.ts b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.ts index 3f1dcf8c3..9e26fe912 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.ts +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-list/motion-block-list.component.ts @@ -36,9 +36,9 @@ export class MotionBlockListComponent public createBlockForm: FormGroup; /** - * The new motion block to create + * Flag, if the creation panel is open */ - public blockToCreate: MotionBlock | null; + public isCreatingNewBlock = false; /** * Holds the agenda items to select the parent item @@ -99,7 +99,8 @@ export class MotionBlockListComponent this.createBlockForm = this.formBuilder.group({ title: ['', Validators.required], agenda_type: ['', Validators.required], - agenda_parent_id: [] + agenda_parent_id: [], + internal: [false] }); } @@ -164,9 +165,9 @@ export class MotionBlockListComponent * Click handler for the plus button */ public onPlusButton(): void { - if (!this.blockToCreate) { + if (!this.isCreatingNewBlock) { this.resetForm(); - this.blockToCreate = new MotionBlock(); + this.isCreatingNewBlock = true; } } @@ -176,15 +177,18 @@ export class MotionBlockListComponent */ public onSaveNewButton(): void { if (this.createBlockForm.valid) { - const blockPatch = this.createBlockForm.value; - if (!blockPatch.agenda_parent_id) { - delete blockPatch.agenda_parent_id; + const block = this.createBlockForm.value; + if (!block.agenda_parent_id) { + delete block.agenda_parent_id; } - this.blockToCreate.patchValues(blockPatch); - this.repo.create(this.blockToCreate); - this.resetForm(); - this.blockToCreate = null; + try { + this.repo.create(block); + this.resetForm(); + this.isCreatingNewBlock = false; + } catch (e) { + this.raiseError(e); + } } // set a form control as "touched" to trigger potential error messages this.createBlockForm.get('title').markAsTouched(); @@ -209,6 +213,6 @@ export class MotionBlockListComponent * Cancels the current form action */ public onCancel(): void { - this.blockToCreate = null; + this.isCreatingNewBlock = false; } } diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py index b514d7b1b..f718d2eff 100644 --- a/openslides/motions/access_permissions.py +++ b/openslides/motions/access_permissions.py @@ -150,6 +150,20 @@ class MotionBlockAccessPermissions(BaseAccessPermissions): base_permission = "motions.can_see" + async def get_restricted_data( + self, full_data: List[Dict[str, Any]], user_id: int + ) -> List[Dict[str, Any]]: + """ + Users without `motions.can_manage` cannot see internal blocks. + """ + data: List[Dict[str, Any]] = [] + if await async_has_perm(user_id, "motions.can_manage"): + data = full_data + else: + data = [full for full in full_data if not full["internal"]] + + return data + class WorkflowAccessPermissions(BaseAccessPermissions): """ diff --git a/openslides/motions/migrations/0027_motion_block_internal.py b/openslides/motions/migrations/0027_motion_block_internal.py new file mode 100644 index 000000000..b6bd36d36 --- /dev/null +++ b/openslides/motions/migrations/0027_motion_block_internal.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2 on 2019-05-21 06:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("motions", "0026_rename_restriction")] + + operations = [ + migrations.AddField( + model_name="motionblock", + name="internal", + field=models.BooleanField(default=False), + ) + ] diff --git a/openslides/motions/models.py b/openslides/motions/models.py index b190e351d..ae2cb6006 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -833,6 +833,12 @@ class MotionBlock(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Mode title = models.CharField(max_length=255) + internal = models.BooleanField(default=False) + """ + If a motion block is internal, only users with `motions.can_manage` can see and + manage these blocks. + """ + class Meta: verbose_name = "Motion block" default_permissions = () diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index a94335f8b..fdfae7ead 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -82,6 +82,7 @@ class MotionBlockSerializer(ModelSerializer): "list_of_speakers_id", "agenda_type", "agenda_parent_id", + "internal", ) def create(self, validated_data): diff --git a/tests/integration/motions/test_viewset.py b/tests/integration/motions/test_viewset.py index d6c203760..965ce01e0 100644 --- a/tests/integration/motions/test_viewset.py +++ b/tests/integration/motions/test_viewset.py @@ -1867,6 +1867,66 @@ class NumberMotionsInCategory(TestCase): ) +class TestMotionBlock(TestCase): + def setUp(self): + self.client = APIClient() + self.client.login(username="admin", password="admin") + + def make_admin_delegate(self): + admin = get_user_model().objects.get(username="admin") + admin.groups.add(GROUP_DELEGATE_PK) + admin.groups.remove(GROUP_ADMIN_PK) + inform_changed_data(admin) + + def test_creation(self): + response = self.client.post( + reverse("motionblock-list"), {"title": "test_title_r23098OMFwoqof3if3kO"} + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertTrue(MotionBlock.objects.exists()) + self.assertEqual( + MotionBlock.objects.get().title, "test_title_r23098OMFwoqof3if3kO" + ) + + def test_creation_no_data(self): + response = self.client.post(reverse("motionblock-list"), {}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertFalse(MotionBlock.objects.exists()) + + def test_creation_not_authenticated(self): + self.make_admin_delegate() + response = self.client.post( + reverse("motionblock-list"), {"title": "test_title_2PFjpf39ap,38fuMPO§8"} + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertFalse(MotionBlock.objects.exists()) + + def test_retrieve_simple(self): + motion_block = MotionBlock(title="test_title") + motion_block.save() + + response = self.client.get( + reverse("motionblock-detail", args=[motion_block.pk]) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + sorted(response.data.keys()), + sorted( + ("agenda_item_id", "id", "internal", "list_of_speakers_id", "title") + ), + ) + + def test_retrieve_internal_non_admin(self): + self.make_admin_delegate() + motion_block = MotionBlock(title="test_title", internal=True) + motion_block.save() + + response = self.client.get( + reverse("motionblock-detail", args=[motion_block.pk]) + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + class FollowRecommendationsForMotionBlock(TestCase): """ Tests following the recommendations of motions in an motion block.