From c8307e73880c38abf54ee6b4dd4d53193e19adf8 Mon Sep 17 00:00:00 2001 From: Halsey Burgund Date: Tue, 19 Nov 2024 15:46:59 -0500 Subject: [PATCH 1/6] add new self-referential many-to-many speaker field to handle parent child relationships; includes migration --- .../rw/migrations/0040_speaker_parents.py | 18 ++++++++++++++++++ roundware/rw/models.py | 2 ++ 2 files changed, 20 insertions(+) create mode 100644 roundware/rw/migrations/0040_speaker_parents.py diff --git a/roundware/rw/migrations/0040_speaker_parents.py b/roundware/rw/migrations/0040_speaker_parents.py new file mode 100644 index 00000000..d3eb99b7 --- /dev/null +++ b/roundware/rw/migrations/0040_speaker_parents.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0 on 2024-11-16 12:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rw', '0039_uiitem_filter_remove_validation'), + ] + + operations = [ + migrations.AddField( + model_name='speaker', + name='parents', + field=models.ManyToManyField(blank=True, related_name='children', to='rw.Speaker'), + ), + ] diff --git a/roundware/rw/models.py b/roundware/rw/models.py index 54dea9d1..d83a4af9 100644 --- a/roundware/rw/models.py +++ b/roundware/rw/models.py @@ -546,6 +546,8 @@ def __init__(self, *args, **kwargs): attenuation_distance = models.IntegerField() attenuation_border = models.GeometryField(geography=True, null=True, editable=False) + parents = models.ManyToManyField('Speaker', related_name='children', symmetrical=False, blank=True) + objects = GeoManager() def __str__(self): From 219c76538923cff90eefb8bbdaebe952d63e1bfc Mon Sep 17 00:00:00 2001 From: Halsey Burgund Date: Tue, 19 Nov 2024 15:51:24 -0500 Subject: [PATCH 2/6] efficiently include derived field 'children' in speakers/ output --- roundware/api2/serializers.py | 6 ++++++ roundware/api2/views.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/roundware/api2/serializers.py b/roundware/api2/serializers.py index e04416fa..d07b4d8c 100644 --- a/roundware/api2/serializers.py +++ b/roundware/api2/serializers.py @@ -342,10 +342,16 @@ def to_representation(self, obj): class SpeakerSerializer(serializers.ModelSerializer): + children = serializers.SerializerMethodField() + class Meta: model = Speaker fields = "__all__" + def get_children(self, obj): + # Access the reverse relationship via `children` + return [child.id for child in obj.children.all()] # Return IDs of children + def to_representation(self, obj): result = super(SpeakerSerializer, self).to_representation(obj) result["project_id"] = result["project"] diff --git a/roundware/api2/views.py b/roundware/api2/views.py index d500df70..095203f0 100644 --- a/roundware/api2/views.py +++ b/roundware/api2/views.py @@ -1215,8 +1215,8 @@ class SpeakerViewSet(viewsets.ViewSet): API V2: api/2/speakers/ api/2/speakers/:id/ """ - queryset = Speaker.objects.all() - permission_classes = (IsAuthenticated,) + queryset = Speaker.objects.prefetch_related('children', 'parents') + permission_classes = (IsAuthenticated, ) def get_object(self, pk): try: From 77aa608d9af0748214ccb3e043a4e561bb7a9e56 Mon Sep 17 00:00:00 2001 From: Halsey Burgund Date: Tue, 19 Nov 2024 15:51:49 -0500 Subject: [PATCH 3/6] add speaker filtering by parents and children --- roundware/api2/filters.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/roundware/api2/filters.py b/roundware/api2/filters.py index 6afe304a..7923cf9e 100644 --- a/roundware/api2/filters.py +++ b/roundware/api2/filters.py @@ -242,10 +242,14 @@ class Meta: class SpeakerFilterSet(django_filters.FilterSet): activeyn = django_filters.TypedChoiceFilter(choices=BOOLEAN_CHOICES, coerce=strtobool) project_id = django_filters.NumberFilter() + parent_ids_or = IntegerListFilter(field_name='parents', lookup_expr='in') # performs OR filtering + parent_ids = IntegerListAndFilter(field_name='parents__id') # performs AND filtering + children_ids_or = IntegerListFilter(field_name='children', lookup_expr='in') # performs OR filtering + children_ids = IntegerListAndFilter(field_name='children__id') # performs AND filtering class Meta: model = Speaker - fields = ["activeyn", "project_id"] + fields = ["activeyn", "project_id", "parents", "children"] class TagFilterSet(django_filters.FilterSet): From 3de6aa4b4232e88762b8cd2e9557b2d945490a47 Mon Sep 17 00:00:00 2001 From: Halsey Burgund Date: Tue, 19 Nov 2024 15:52:09 -0500 Subject: [PATCH 4/6] update Django admin to manage parents field --- roundware/rw/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roundware/rw/admin.py b/roundware/rw/admin.py index 1b13f3b8..b5c5c849 100644 --- a/roundware/rw/admin.py +++ b/roundware/rw/admin.py @@ -493,6 +493,7 @@ class SpeakerAdmin(LeafletGeoAdmin, ProjectProtectedModelAdmin): list_display = ('id', 'activeyn', 'code', 'project', 'maxvolume', 'minvolume', 'shape', 'uri') list_filter = ('project', 'activeyn') list_editable = ('activeyn', 'maxvolume', 'minvolume', 'shape') + filter_horizontal = ('parents', ) ordering = ['id'] save_as = True save_on_top = True @@ -500,7 +501,7 @@ class SpeakerAdmin(LeafletGeoAdmin, ProjectProtectedModelAdmin): fieldsets = ( (None, { - 'fields': ('activeyn', 'code', 'project', 'maxvolume', 'minvolume', 'uri', ) + 'fields': ('activeyn', 'code', 'project', 'maxvolume', 'minvolume', 'uri', 'parents' ) }), ('Geographical Data', { 'fields': ('shape', 'attenuation_distance'), From c47bcd739eb8bc7eaa50b36e62cf668eeb194bec Mon Sep 17 00:00:00 2001 From: Halsey Burgund Date: Tue, 19 Nov 2024 15:52:42 -0500 Subject: [PATCH 5/6] add basic speaker.parents and children unit tests --- roundware/rw/tests/test_models.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/roundware/rw/tests/test_models.py b/roundware/rw/tests/test_models.py index db07ab34..8f3e3c7b 100644 --- a/roundware/rw/tests/test_models.py +++ b/roundware/rw/tests/test_models.py @@ -107,3 +107,19 @@ def test_get_flags(self): def test_distance(self): distance = self.asset1.distance({'latitude': 0, 'longitude': 0}) + +class TestSpeaker(RWTestCase): + + def setUp(self): + super().setUp() + + self.project = baker.make('rw.Project') + self.speaker1 = baker.make('rw.Speaker', project=self.project) + self.speaker2 = baker.make('rw.Speaker', project=self.project, parents=[self.speaker1]) + + def test_speaker_children(self): + self.assertEqual(self.speaker1.parents.count(), 0) + self.assertEqual(self.speaker2.parents.count(), 1) + self.assertEqual(self.speaker1.children.count(), 1) + self.assertEqual(self.speaker2.children.count(), 0) + From e73ac96ef2d69d285883dda173a6eab27175e0ba Mon Sep 17 00:00:00 2001 From: Halsey Burgund Date: Wed, 20 Nov 2024 17:09:53 -0500 Subject: [PATCH 6/6] refactor speaker children representation --- roundware/api2/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roundware/api2/serializers.py b/roundware/api2/serializers.py index d07b4d8c..d29e6155 100644 --- a/roundware/api2/serializers.py +++ b/roundware/api2/serializers.py @@ -350,7 +350,8 @@ class Meta: def get_children(self, obj): # Access the reverse relationship via `children` - return [child.id for child in obj.children.all()] # Return IDs of children + return obj.children.values_list('id', flat=True) # Return IDs of children + def to_representation(self, obj): result = super(SpeakerSerializer, self).to_representation(obj)