diff --git a/client/src/app/core/repositories/motions/category-repository.service.ts b/client/src/app/core/repositories/motions/category-repository.service.ts index e3ff051fd..d6d2f46e1 100644 --- a/client/src/app/core/repositories/motions/category-repository.service.ts +++ b/client/src/app/core/repositories/motions/category-repository.service.ts @@ -70,24 +70,28 @@ export class CategoryRepositoryService extends BaseRepository(Category, cat => cat.id === category_id); - } - /** * Updates a categories numbering. + * * @param category the category it should be updated in - * @param motionList the list of motions on this category + * @param motionIds the list of motion ids on this category */ public async numberMotionsInCategory(category: Category, motionIds: number[]): Promise { const collectionString = 'rest/motions/category/' + category.id + '/numbering/'; await this.httpService.post(collectionString, { motions: motionIds }); } + /** + * Updates the sorting of motions in a category. + * + * @param category the category it should be updated in + * @param motionIds the list of motion ids on this category + */ + public async sortMotionsInCategory(category: Category, motionIds: number[]): Promise { + const collectionString = 'rest/motions/category/' + category.id + '/sort/'; + await this.httpService.post(collectionString, { motions: motionIds }); + } + /** * Triggers an update for the sort function responsible for the default sorting of data items */ diff --git a/client/src/app/shared/models/motions/motion.ts b/client/src/app/shared/models/motions/motion.ts index 8bbeb0950..d9ffb3847 100644 --- a/client/src/app/shared/models/motions/motion.ts +++ b/client/src/app/shared/models/motions/motion.ts @@ -28,6 +28,7 @@ export class Motion extends BaseModel { public modified_final_version: string; public parent_id: number; public category_id: number; + public category_weight: number; public motion_block_id: number; public origin: string; public submitters: MotionSubmitter[]; diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index 50fe69337..86926fe60 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -128,6 +128,10 @@ export class ViewMotion extends BaseAgendaViewModel implements Searchable { return this._category; } + public get category_weight(): number { + return this.motion.category_weight; + } + public get submitters(): ViewUser[] { return this._submitters; } diff --git a/openslides/motions/migrations/0025_motion_category_weight.py b/openslides/motions/migrations/0025_motion_category_weight.py new file mode 100644 index 000000000..7dfacb89a --- /dev/null +++ b/openslides/motions/migrations/0025_motion_category_weight.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2 on 2019-04-30 11:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("motions", "0024_state_restriction_3")] + + operations = [ + migrations.AddField( + model_name="motion", + name="category_weight", + field=models.IntegerField(default=10000), + ) + ] diff --git a/openslides/motions/models.py b/openslides/motions/models.py index e198ed4c8..429162caf 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -187,6 +187,12 @@ class Motion(RESTModelMixin, models.Model): ForeignKey to one category of motions. """ + category_weight = models.IntegerField(default=10000) + """ + Sorts the motions inside a category. Default is 10000 so new motions + in a category will be added on the end of the list. + """ + motion_block = models.ForeignKey( "MotionBlock", on_delete=SET_NULL_AND_AUTOUPDATE, null=True, blank=True ) diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index 7fd5ec1b3..21aaa6f86 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -428,6 +428,7 @@ class MotionSerializer(ModelSerializer): "reason", "parent", "category", + "category_weight", "comments", "motion_block", "origin", @@ -455,6 +456,8 @@ class MotionSerializer(ModelSerializer): read_only_fields = ( "state", "recommendation", + "weight", + "category_weight", ) # Some other fields are also read_only. See definitions above. def validate(self, data): @@ -527,16 +530,34 @@ class MotionSerializer(ModelSerializer): def update(self, motion, validated_data): """ Customized method to update a motion. + - If the workflow changes, the state of the motions is resetted to + the initial state of the new workflow. + - If the category changes, the category_weight is reset to the default value. """ workflow_id = None if "workflow_id" in validated_data: workflow_id = validated_data.pop("workflow_id") + old_category_id = motion.category.pk if motion.category is not None else None + new_category_id = ( + validated_data["category"].pk + if validated_data.get("category") is not None + else None + ) + result = super().update(motion, validated_data) + # Check for changed workflow if workflow_id is not None and workflow_id != motion.workflow_id: motion.reset_state(workflow_id) - motion.save() + motion.save(skip_autoupdate=True) + + # Check for changed category + if old_category_id != new_category_id: + motion.category_weight = 10000 + motion.save(skip_autoupdate=True) + + inform_changed_data(motion) return result diff --git a/openslides/motions/views.py b/openslides/motions/views.py index cdd8ec09a..b4209aba3 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -1,5 +1,5 @@ import re -from typing import List +from typing import List, Set import jsonschema from django.conf import settings @@ -1158,6 +1158,7 @@ class CategoryViewSet(ModelViewSet): "partial_update", "update", "destroy", + "sort", "numbering", ): result = has_perm(self.request.user, "motions.can_see") and has_perm( @@ -1167,6 +1168,52 @@ class CategoryViewSet(ModelViewSet): result = False return result + @detail_route(methods=["post"]) + @transaction.atomic + def sort(self, request, pk=None): + """ + Endpoint to sort all motions in the category. + + Send POST {'motions': []} to sort the given + motions in the given order. Ids of motions with another category or + non existing motions are ignored, but all motions of this category + have to be send. + """ + category = self.get_object() + + ids = request.data.get("motions", None) + if not isinstance(ids, list): + raise ValidationError("The ids must be a list.") + + motions = [] + motion_ids: Set[int] = set() # To detect duplicated + for id in ids: + if not isinstance(id, int): + raise ValidationError("All ids must be int.") + + if id in motion_ids: + continue # Duplicate id + + try: + motion = Motion.objects.get(pk=id) + except Motion.DoesNotExist: + continue # Ignore invalid ids. + + if motion.category is not None and motion.category.pk == category.pk: + motions.append(motion) + motion_ids.add(id) + + if Motion.objects.filter(category=category).count() != len(motions): + raise ValidationError("Not all motions for this category are given") + + # assign the category_weight field: + for weight, motion in enumerate(motions, start=1): + motion.category_weight = weight + motion.save(skip_autoupdate=True) + + inform_changed_data(motions) + return Response() + @detail_route(methods=["post"]) def numbering(self, request, pk=None): """ diff --git a/tests/integration/motions/test_viewset.py b/tests/integration/motions/test_viewset.py index 65f6cbe27..4e2b2e037 100644 --- a/tests/integration/motions/test_viewset.py +++ b/tests/integration/motions/test_viewset.py @@ -582,6 +582,26 @@ class UpdateMotion(TestCase): self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh") self.assertEqual(motion.workflow_id, 2) + def test_patch_category(self): + """ + Tests to only update the category of a motion. Expects the + category_weight to be resetted. + """ + category = Category.objects.create( + name="test_category_name_FE3jO(Fm83doqqlwcvlv", + prefix="test_prefix_w3ofg2mv79UGFqjk3f8h", + ) + self.motion.category_weight = 1 + self.motion.save() + response = self.client.patch( + reverse("motion-detail", args=[self.motion.pk]), + {"category_id": category.pk}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + motion = Motion.objects.get() + self.assertEqual(motion.category, category) + self.assertEqual(motion.category_weight, 10000) + def test_patch_supporters(self): supporter = get_user_model().objects.create_user( username="test_username_ieB9eicah0uqu6Phoovo",