Merge pull request #5389 from jsangmeister/reverse-motion-relations

Adds reverse relations for motions and blocks
This commit is contained in:
Finn Stutzenstein 2020-06-02 16:26:07 +02:00 committed by GitHub
commit e215a23b80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 156 additions and 48 deletions

View File

@ -84,6 +84,7 @@ class MotionManager(BaseManager):
"submitters", "submitters",
"supporters", "supporters",
"change_recommendations", "change_recommendations",
"amendments",
) )
) )
@ -192,7 +193,7 @@ class Motion(RESTModelMixin, AgendaItemWithListOfSpeakersMixin, models.Model):
""" """
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,
) )
""" """
ForeignKey to one block of motions. ForeignKey to one block of motions.
@ -821,7 +822,7 @@ class MotionBlockManager(BaseManager):
return ( return (
super() super()
.get_prefetched_queryset(*args, **kwargs) .get_prefetched_queryset(*args, **kwargs)
.prefetch_related("agenda_items", "lists_of_speakers") .prefetch_related("agenda_items", "lists_of_speakers", "motion_set")
) )

View File

@ -69,25 +69,24 @@ async def get_amendment_merge_into_motion_final(all_data_provider, amendment):
async def get_amendments_for_motion(motion, all_data_provider): async def get_amendments_for_motion(motion, all_data_provider):
amendment_data = [] amendment_data = []
all_motions = await all_data_provider.get_collection("motions/motion") for amendment_id in motion["amendments_id"]:
for amendment_id, amendment in all_motions.items(): amendment = await all_data_provider.get("motions/motion", amendment_id)
if amendment["parent_id"] == motion["id"]: merge_amendment_into_final = await get_amendment_merge_into_motion_final(
merge_amendment_into_final = await get_amendment_merge_into_motion_final( all_data_provider, amendment
all_data_provider, amendment )
) merge_amendment_into_diff = await get_amendment_merge_into_motion_diff(
merge_amendment_into_diff = await get_amendment_merge_into_motion_diff( all_data_provider, amendment
all_data_provider, amendment )
) amendment_data.append(
amendment_data.append( {
{ "id": amendment["id"],
"id": amendment["id"], "identifier": amendment["identifier"],
"identifier": amendment["identifier"], "title": amendment["title"],
"title": amendment["title"], "amendment_paragraphs": amendment["amendment_paragraphs"],
"amendment_paragraphs": amendment["amendment_paragraphs"], "merge_amendment_into_diff": merge_amendment_into_diff,
"merge_amendment_into_diff": merge_amendment_into_diff, "merge_amendment_into_final": merge_amendment_into_final,
"merge_amendment_into_final": merge_amendment_into_final, }
} )
)
return amendment_data return amendment_data
@ -334,32 +333,37 @@ async def motion_block_slide(
# All title information for referenced motions in the recommendation # All title information for referenced motions in the recommendation
referenced_motions: Dict[int, Dict[str, str]] = {} referenced_motions: Dict[int, Dict[str, str]] = {}
# Search motions. # iterate motions.
all_motions = await all_data_provider.get_collection("motions/motion") for motion_id in motion_block["motions_id"]:
for motion in all_motions.values(): motion = await all_data_provider.get("motions/motion", motion_id)
if motion["motion_block_id"] == motion_block["id"]: # primarily to please mypy, should theoretically not happen
motion_object = { if motion is None:
"title": motion["title"], raise RuntimeError(
"identifier": motion["identifier"], f"motion {motion_id} of block {element.get('id')} could not be found"
)
motion_object = {
"title": motion["title"],
"identifier": motion["identifier"],
}
recommendation_id = motion["recommendation_id"]
if recommendation_id is not None:
recommendation = await get_state(
all_data_provider, motion, "recommendation_id"
)
motion_object["recommendation"] = {
"name": recommendation["recommendation_label"],
"css_class": recommendation["css_class"],
} }
if recommendation["show_recommendation_extension_field"]:
recommendation_id = motion["recommendation_id"] recommendation_extension = motion["recommendation_extension"]
if recommendation_id is not None: await extend_reference_motion_dict(
recommendation = await get_state( all_data_provider, recommendation_extension, referenced_motions
all_data_provider, motion, "recommendation_id"
) )
motion_object["recommendation"] = { motion_object["recommendation_extension"] = recommendation_extension
"name": recommendation["recommendation_label"],
"css_class": recommendation["css_class"],
}
if recommendation["show_recommendation_extension_field"]:
recommendation_extension = motion["recommendation_extension"]
await extend_reference_motion_dict(
all_data_provider, recommendation_extension, referenced_motions
)
motion_object["recommendation_extension"] = recommendation_extension
motions.append(motion_object) motions.append(motion_object)
return { return {
"title": motion_block["title"], "title": motion_block["title"],

View File

@ -83,6 +83,7 @@ class MotionBlockSerializer(ModelSerializer):
write_only=True, required=False, min_value=1, max_value=3, allow_null=True write_only=True, required=False, min_value=1, max_value=3, allow_null=True
) )
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
motions_id = SerializerMethodField()
class Meta: class Meta:
model = MotionBlock model = MotionBlock
@ -95,8 +96,12 @@ class MotionBlockSerializer(ModelSerializer):
"agenda_type", "agenda_type",
"agenda_parent_id", "agenda_parent_id",
"internal", "internal",
"motions_id",
) )
def get_motions_id(self, block):
return [motion.id for motion in block.motion_set.all()]
def create(self, validated_data): def create(self, validated_data):
""" """
Customized create method. Set information about related agenda item Customized create method. Set information about related agenda item
@ -371,6 +376,7 @@ class MotionSerializer(ModelSerializer):
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
submitters = SubmitterSerializer(many=True, read_only=True) submitters = SubmitterSerializer(many=True, read_only=True)
change_recommendations = IdPrimaryKeyRelatedField(many=True, read_only=True) change_recommendations = IdPrimaryKeyRelatedField(many=True, read_only=True)
amendments_id = SerializerMethodField()
class Meta: class Meta:
model = Motion model = Motion
@ -409,14 +415,19 @@ class MotionSerializer(ModelSerializer):
"created", "created",
"last_modified", "last_modified",
"change_recommendations", "change_recommendations",
"amendments_id",
) )
read_only_fields = ( read_only_fields = (
"state", "state",
"recommendation", "recommendation",
"weight", "weight",
"category_weight", "category_weight",
"amendments_id",
) # Some other fields are also read_only. See definitions above. ) # Some other fields are also read_only. See definitions above.
def get_amendments_id(self, motion):
return [amendment.id for amendment in motion.amendments.all()]
def validate(self, data): def validate(self, data):
if "text" in data: if "text" in data:
data["text"] = validate_html_strict(data["text"]) data["text"] = validate_html_strict(data["text"])
@ -488,6 +499,12 @@ class MotionSerializer(ModelSerializer):
motion.supporters.add(*validated_data.get("supporters", [])) motion.supporters.add(*validated_data.get("supporters", []))
motion.attachments.add(*validated_data.get("attachments", [])) motion.attachments.add(*validated_data.get("attachments", []))
motion.tags.add(*validated_data.get("tags", [])) motion.tags.add(*validated_data.get("tags", []))
if motion.parent:
inform_changed_data(motion.parent)
if motion.motion_block:
inform_changed_data(motion.motion_block)
return motion return motion
@transaction.atomic @transaction.atomic
@ -508,6 +525,8 @@ class MotionSerializer(ModelSerializer):
if validated_data.get("category") is not None if validated_data.get("category") is not None
else None else None
) )
old_block = motion.motion_block
new_block = validated_data.get("motion_block")
result = super().update(motion, validated_data) result = super().update(motion, validated_data)
@ -523,6 +542,12 @@ class MotionSerializer(ModelSerializer):
inform_changed_data(motion) inform_changed_data(motion)
if new_block != old_block:
if new_block:
inform_changed_data(new_block)
if old_block:
inform_changed_data(old_block)
return result return result
def get_state_restriction(self, motion): def get_state_restriction(self, motion):

View File

@ -128,6 +128,12 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
user_id=request.user.pk, user_id=request.user.pk,
) )
# inform parents/blocks of deletion
if motion.parent:
inform_changed_data(motion.parent)
if motion.motion_block:
inform_changed_data(motion.motion_block)
return result return result
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -675,6 +681,13 @@ class MotionViewSet(TreeSortMixin, ModelViewSet):
} }
) )
# inform old motion block
if motion.motion_block:
inform_changed_data(motion.motion_block)
# inform new motion block
if motion_block:
inform_changed_data(motion_block)
# Set motion bock # Set motion bock
motion.motion_block = motion_block motion.motion_block = motion_block

View File

@ -36,6 +36,7 @@ def test_motion_db_queries():
* 1 request for all motion comments * 1 request for all motion comments
* 1 request for all motion comment sections required for the comments * 1 request for all motion comment sections required for the comments
* 1 request for all users required for the read_groups of the sections * 1 request for all users required for the read_groups of the sections
* 1 request to get all amendments of all motions
* 1 request to get the agenda item, * 1 request to get the agenda item,
* 1 request to get the list of speakers, * 1 request to get the list of speakers,
* 1 request to get the attachments, * 1 request to get the attachments,
@ -90,7 +91,7 @@ def test_motion_db_queries():
) )
poll.create_options() poll.create_options()
assert count_queries(Motion.get_elements)() == 12 assert count_queries(Motion.get_elements)() == 13
class CreateMotion(TestCase): class CreateMotion(TestCase):
@ -109,7 +110,7 @@ class CreateMotion(TestCase):
The created motion should have an identifier and the admin user should The created motion should have an identifier and the admin user should
be the submitter. be the submitter.
""" """
with self.assertNumQueries(52): with self.assertNumQueries(54):
response = self.client.post( response = self.client.post(
reverse("motion-list"), reverse("motion-list"),
{ {
@ -141,6 +142,7 @@ class CreateMotion(TestCase):
"title": "test_title_OoCoo3MeiT9li5Iengu9", "title": "test_title_OoCoo3MeiT9li5Iengu9",
"text": "test_text_thuoz0iecheiheereiCi", "text": "test_text_thuoz0iecheiheereiCi",
"amendment_paragraphs": None, "amendment_paragraphs": None,
"amendments_id": [],
"modified_final_version": "", "modified_final_version": "",
"reason": "", "reason": "",
"parent_id": None, "parent_id": None,

View File

@ -53,13 +53,32 @@ def test_statute_paragraph_db_queries():
def test_workflow_db_queries(): def test_workflow_db_queries():
""" """
Tests that only the following db queries are done: Tests that only the following db queries are done:
* 1 requests to get the list of all workflows and * 1 request to get the list of all workflows and
* 1 request to get all states. * 1 request to get all states.
""" """
assert count_queries(Workflow.get_elements)() == 2 assert count_queries(Workflow.get_elements)() == 2
@pytest.mark.django_db(transaction=False)
def test_motion_block_db_queries():
"""
Tests that only the following db queries are done:
* 1 request to get all motion blocks
* 1 request to get all agenda items
* 1 request to get all lists of speakers
* 1 request to get all motions
"""
for i in range(5):
motion_block = MotionBlock.objects.create(title=f"block{i}")
for j in range(3):
Motion.objects.create(
title=f"motion{i}_{j}", text="text", motion_block=motion_block
)
assert count_queries(MotionBlock.get_elements)() == 4
class TestStatuteParagraphs(TestCase): class TestStatuteParagraphs(TestCase):
""" """
Tests all CRUD operations of statute paragraphs. Tests all CRUD operations of statute paragraphs.
@ -1100,7 +1119,14 @@ class TestMotionBlock(TestCase):
self.assertEqual( self.assertEqual(
sorted(response.data.keys()), sorted(response.data.keys()),
sorted( sorted(
("agenda_item_id", "id", "internal", "list_of_speakers_id", "title") (
"agenda_item_id",
"id",
"internal",
"list_of_speakers_id",
"title",
"motions_id",
)
), ),
) )

View File

@ -77,3 +77,37 @@ RESTRICTED_DATA_CACHE = False
REST_FRAMEWORK = {"TEST_REQUEST_DEFAULT_FORMAT": "json"} REST_FRAMEWORK = {"TEST_REQUEST_DEFAULT_FORMAT": "json"}
ENABLE_ELECTRONIC_VOTING = True ENABLE_ELECTRONIC_VOTING = True
# https://stackoverflow.com/questions/24876343/django-traceback-on-queries
if os.environ.get("DEBUG_SQL_TRACEBACK"):
import traceback
import django.db.backends.utils as bakutils
cursor_debug_wrapper_orig = bakutils.CursorDebugWrapper
def print_stack_in_project(sql):
stack = traceback.extract_stack()
for path, lineno, func, line in stack:
if "lib/python" in path or "settings.py" in path:
continue
print(f'File "{path}", line {lineno}, in {func}')
print(f" {line}")
print(sql)
print("\n")
class CursorDebugWrapperLoud(cursor_debug_wrapper_orig): # type: ignore
def execute(self, sql, params=None):
try:
return super().execute(sql, params)
finally:
sql = self.db.ops.last_executed_query(self.cursor, sql, params)
print_stack_in_project(sql)
def executemany(self, sql, param_list):
try:
return super().executemany(sql, param_list)
finally:
print_stack_in_project(sql)
bakutils.CursorDebugWrapper = CursorDebugWrapperLoud

View File

@ -74,6 +74,7 @@ def all_data_provider():
"created": "2019-01-19T18:37:34.741336+01:00", "created": "2019-01-19T18:37:34.741336+01:00",
"last_modified": "2019-01-19T18:37:34.741368+01:00", "last_modified": "2019-01-19T18:37:34.741368+01:00",
"change_recommendations_id": [1, 2], "change_recommendations_id": [1, 2],
"amendments_id": [2],
}, },
2: { 2: {
"id": 2, "id": 2,
@ -107,6 +108,7 @@ def all_data_provider():
"created": "2019-01-19T18:37:34.741336+01:00", "created": "2019-01-19T18:37:34.741336+01:00",
"last_modified": "2019-01-19T18:37:34.741368+01:00", "last_modified": "2019-01-19T18:37:34.741368+01:00",
"change_recommendations": [], "change_recommendations": [],
"amendments_id": [],
}, },
3: { 3: {
"id": 3, "id": 3,
@ -140,6 +142,7 @@ def all_data_provider():
"created": "2019-01-19T18:37:34.741336+01:00", "created": "2019-01-19T18:37:34.741336+01:00",
"last_modified": "2019-01-19T18:37:34.741368+01:00", "last_modified": "2019-01-19T18:37:34.741368+01:00",
"change_recommendations": [], "change_recommendations": [],
"amendments_id": [],
}, },
} }
data["motions/workflow"] = { data["motions/workflow"] = {