Merge pull request #4652 from FinnStutzenstein/sortMotionInCategories
Sort motions in categories
This commit is contained in:
commit
524ff4a981
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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[];
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
16
openslides/motions/migrations/0025_motion_category_weight.py
Normal file
16
openslides/motions/migrations/0025_motion_category_weight.py
Normal 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),
|
||||||
|
)
|
||||||
|
]
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user