Merge pull request #4652 from FinnStutzenstein/sortMotionInCategories

Sort motions in categories
This commit is contained in:
Sean 2019-04-30 14:28:09 +02:00 committed by GitHub
commit 524ff4a981
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 130 additions and 11 deletions

View File

@ -70,24 +70,28 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
return viewCategory; return viewCategory;
} }
/**
* Returns the category for the ID
* @param category_id category ID
*/
public getCategoryByID(category_id: number): Category {
return this.DS.find<Category>(Category, cat => cat.id === category_id);
}
/** /**
* Updates a categories numbering. * Updates a categories numbering.
*
* @param category the category it should be updated in * @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<void> { public async numberMotionsInCategory(category: Category, motionIds: number[]): Promise<void> {
const collectionString = 'rest/motions/category/' + category.id + '/numbering/'; const collectionString = 'rest/motions/category/' + category.id + '/numbering/';
await this.httpService.post(collectionString, { motions: motionIds }); 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<void> {
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 * Triggers an update for the sort function responsible for the default sorting of data items
*/ */

View File

@ -28,6 +28,7 @@ export class Motion extends BaseModel {
public modified_final_version: string; public modified_final_version: string;
public parent_id: number; public parent_id: number;
public category_id: number; public category_id: number;
public category_weight: number;
public motion_block_id: number; public motion_block_id: number;
public origin: string; public origin: string;
public submitters: MotionSubmitter[]; public submitters: MotionSubmitter[];

View File

@ -128,6 +128,10 @@ export class ViewMotion extends BaseAgendaViewModel implements Searchable {
return this._category; return this._category;
} }
public get category_weight(): number {
return this.motion.category_weight;
}
public get submitters(): ViewUser[] { public get submitters(): ViewUser[] {
return this._submitters; return this._submitters;
} }

View File

@ -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),
)
]

View File

@ -187,6 +187,12 @@ class Motion(RESTModelMixin, models.Model):
ForeignKey to one category of motions. 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( motion_block = models.ForeignKey(
"MotionBlock", on_delete=SET_NULL_AND_AUTOUPDATE, null=True, blank=True "MotionBlock", on_delete=SET_NULL_AND_AUTOUPDATE, null=True, blank=True
) )

View File

@ -428,6 +428,7 @@ class MotionSerializer(ModelSerializer):
"reason", "reason",
"parent", "parent",
"category", "category",
"category_weight",
"comments", "comments",
"motion_block", "motion_block",
"origin", "origin",
@ -455,6 +456,8 @@ class MotionSerializer(ModelSerializer):
read_only_fields = ( read_only_fields = (
"state", "state",
"recommendation", "recommendation",
"weight",
"category_weight",
) # Some other fields are also read_only. See definitions above. ) # Some other fields are also read_only. See definitions above.
def validate(self, data): def validate(self, data):
@ -527,16 +530,34 @@ class MotionSerializer(ModelSerializer):
def update(self, motion, validated_data): def update(self, motion, validated_data):
""" """
Customized method to update a motion. 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 workflow_id = None
if "workflow_id" in validated_data: if "workflow_id" in validated_data:
workflow_id = validated_data.pop("workflow_id") 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) result = super().update(motion, validated_data)
# Check for changed workflow
if workflow_id is not None and workflow_id != motion.workflow_id: if workflow_id is not None and workflow_id != motion.workflow_id:
motion.reset_state(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 return result

View File

@ -1,5 +1,5 @@
import re import re
from typing import List from typing import List, Set
import jsonschema import jsonschema
from django.conf import settings from django.conf import settings
@ -1158,6 +1158,7 @@ class CategoryViewSet(ModelViewSet):
"partial_update", "partial_update",
"update", "update",
"destroy", "destroy",
"sort",
"numbering", "numbering",
): ):
result = has_perm(self.request.user, "motions.can_see") and has_perm( result = has_perm(self.request.user, "motions.can_see") and has_perm(
@ -1167,6 +1168,52 @@ class CategoryViewSet(ModelViewSet):
result = False result = False
return result 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': [<list of motion ids>]} 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"]) @detail_route(methods=["post"])
def numbering(self, request, pk=None): def numbering(self, request, pk=None):
""" """

View File

@ -582,6 +582,26 @@ class UpdateMotion(TestCase):
self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh") self.assertEqual(motion.title, "test_title_aeng7ahChie3waiR8xoh")
self.assertEqual(motion.workflow_id, 2) 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): def test_patch_supporters(self):
supporter = get_user_model().objects.create_user( supporter = get_user_model().objects.create_user(
username="test_username_ieB9eicah0uqu6Phoovo", username="test_username_ieB9eicah0uqu6Phoovo",