From 715154e2b52097f907bc2cbf8c96141ad72ad1ed Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Fri, 28 Jan 2022 13:24:05 +0000 Subject: [PATCH 01/11] Add field_list producer --- django_readers/producers.py | 8 ++++++++ tests/test_producers.py | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/django_readers/producers.py b/django_readers/producers.py index a61a268..e78318d 100644 --- a/django_readers/producers.py +++ b/django_readers/producers.py @@ -34,6 +34,14 @@ def producer(instance): return producer +def field_list(relationship_name, field_name): + """ + Given a relationship name and the name of a field, return a producer which returns + a list containing the value of that field for each object in the relationship + """ + return relationship(relationship_name, attrgetter(field_name)) + + def pk_list(name): """ Given an attribute name (which should be a relationship field), return a diff --git a/tests/test_producers.py b/tests/test_producers.py index a04897d..8f2f143 100644 --- a/tests/test_producers.py +++ b/tests/test_producers.py @@ -156,6 +156,18 @@ def hello(self, name): self.assertEqual(result, "hello, tester!") +class FieldListTestCase(TestCase): + def test_field_list(self): + owner = Owner.objects.create(name="test owner") + Widget.objects.create(name="test 1", owner=owner) + Widget.objects.create(name="test 2", owner=owner) + Widget.objects.create(name="test 3", owner=owner) + + produce = producers.field_list("widget_set", "name") + result = produce(owner) + self.assertEqual(result, ["test 1", "test 2", "test 3"]) + + class PKListTestCase(TestCase): def test_pk_list(self): owner = Owner.objects.create(name="test owner") From b7116444488cf195b5221c521a96d6d89d7f6094 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Fri, 28 Jan 2022 13:24:46 +0000 Subject: [PATCH 02/11] pk_list producer now uses field_list --- django_readers/producers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_readers/producers.py b/django_readers/producers.py index e78318d..af4f225 100644 --- a/django_readers/producers.py +++ b/django_readers/producers.py @@ -49,4 +49,4 @@ def pk_list(name): just a single PK if this is a to-one field, but this is an inefficient way of doing it). """ - return relationship(name, attrgetter("pk")) + return field_list(name, "pk") From e11fbba1e09ee3a47e5c811f6ac0ab96d412ded9 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Fri, 28 Jan 2022 13:29:14 +0000 Subject: [PATCH 03/11] Add field_list pair function --- django_readers/pairs.py | 11 +++++++++++ tests/test_pairs.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/django_readers/pairs.py b/django_readers/pairs.py index b4dd5ac..3937ba5 100644 --- a/django_readers/pairs.py +++ b/django_readers/pairs.py @@ -111,6 +111,17 @@ def relationship(name, relationship_pair, to_attr=None): return prepare, producers.relationship(to_attr or name, project_relationship) +def field_list(relationship_name, field_name, to_attr=None): + return ( + qs.auto_prefetch_relationship( + relationship_name, + qs.include_fields(field_name), + to_attr=to_attr, + ), + producers.field_list(to_attr or relationship_name, field_name), + ) + + def pk_list(name, to_attr=None): return ( qs.auto_prefetch_relationship(name, qs.include_fields("pk"), to_attr=to_attr), diff --git a/tests/test_pairs.py b/tests/test_pairs.py index 9e535ab..5f29928 100644 --- a/tests/test_pairs.py +++ b/tests/test_pairs.py @@ -639,6 +639,36 @@ def test_order_by(self): self.assertEqual(result, [{"name": "a"}, {"name": "b"}, {"name": "c"}]) +class FieldListTestCase(TestCase): + def test_field_list(self): + owner = Owner.objects.create(name="test owner") + Widget.objects.create(name="test 1", owner=owner) + Widget.objects.create(name="test 2", owner=owner) + Widget.objects.create(name="test 3", owner=owner) + + prepare, project = pairs.producer_to_projector( + "widget_set", pairs.field_list("widget_set", "name") + ) + + queryset = prepare(Owner.objects.all()) + result = project(queryset.first()) + self.assertEqual(result, {"widget_set": ["test 1", "test 2", "test 3"]}) + + def test_field_list_with_to_attr(self): + owner = Owner.objects.create(name="test owner") + Widget.objects.create(name="test 1", owner=owner) + Widget.objects.create(name="test 2", owner=owner) + Widget.objects.create(name="test 3", owner=owner) + + prepare, project = pairs.producer_to_projector( + "widgets", pairs.field_list("widget_set", "name", to_attr="widgets") + ) + + queryset = prepare(Owner.objects.all()) + result = project(queryset.first()) + self.assertEqual(result, {"widgets": ["test 1", "test 2", "test 3"]}) + + class PKListTestCase(TestCase): def test_pk_list(self): owner = Owner.objects.create(name="test owner") From 5c1b475c71a65b89fb68818f72de52fecc40e08c Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Fri, 28 Jan 2022 13:30:38 +0000 Subject: [PATCH 04/11] pk_list pair function now uses field_list --- django_readers/pairs.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/django_readers/pairs.py b/django_readers/pairs.py index 3937ba5..60bb98c 100644 --- a/django_readers/pairs.py +++ b/django_readers/pairs.py @@ -123,7 +123,4 @@ def field_list(relationship_name, field_name, to_attr=None): def pk_list(name, to_attr=None): - return ( - qs.auto_prefetch_relationship(name, qs.include_fields("pk"), to_attr=to_attr), - producers.pk_list(to_attr or name), - ) + return field_list(name, "pk", to_attr=to_attr) From 2c2f9e153332cbdc74cb3ef0ec2438e33f80d2f7 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Fri, 28 Jan 2022 13:32:04 +0000 Subject: [PATCH 05/11] Improve argument name to pk_list functions --- django_readers/pairs.py | 4 ++-- django_readers/producers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/django_readers/pairs.py b/django_readers/pairs.py index 60bb98c..e0ee5ed 100644 --- a/django_readers/pairs.py +++ b/django_readers/pairs.py @@ -122,5 +122,5 @@ def field_list(relationship_name, field_name, to_attr=None): ) -def pk_list(name, to_attr=None): - return field_list(name, "pk", to_attr=to_attr) +def pk_list(relationship_name, to_attr=None): + return field_list(relationship_name, "pk", to_attr=to_attr) diff --git a/django_readers/producers.py b/django_readers/producers.py index af4f225..805a7f7 100644 --- a/django_readers/producers.py +++ b/django_readers/producers.py @@ -42,11 +42,11 @@ def field_list(relationship_name, field_name): return relationship(relationship_name, attrgetter(field_name)) -def pk_list(name): +def pk_list(relationship_name): """ Given an attribute name (which should be a relationship field), return a producer which returns a list of the PK of each item in the relationship (or just a single PK if this is a to-one field, but this is an inefficient way of doing it). """ - return field_list(name, "pk") + return field_list(relationship_name, "pk") From 0b6f9c531a57012fb64ea2172f068d4f498a9075 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Sat, 29 Jan 2022 21:33:11 +0000 Subject: [PATCH 06/11] Rename field_list to related_field_value --- django_readers/pairs.py | 6 +++--- django_readers/producers.py | 4 ++-- tests/test_pairs.py | 11 ++++++----- tests/test_producers.py | 6 +++--- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/django_readers/pairs.py b/django_readers/pairs.py index e0ee5ed..10e5236 100644 --- a/django_readers/pairs.py +++ b/django_readers/pairs.py @@ -111,16 +111,16 @@ def relationship(name, relationship_pair, to_attr=None): return prepare, producers.relationship(to_attr or name, project_relationship) -def field_list(relationship_name, field_name, to_attr=None): +def related_field_value(relationship_name, field_name, to_attr=None): return ( qs.auto_prefetch_relationship( relationship_name, qs.include_fields(field_name), to_attr=to_attr, ), - producers.field_list(to_attr or relationship_name, field_name), + producers.related_field_value(to_attr or relationship_name, field_name), ) def pk_list(relationship_name, to_attr=None): - return field_list(relationship_name, "pk", to_attr=to_attr) + return related_field_value(relationship_name, "pk", to_attr=to_attr) diff --git a/django_readers/producers.py b/django_readers/producers.py index 805a7f7..0f73a94 100644 --- a/django_readers/producers.py +++ b/django_readers/producers.py @@ -34,7 +34,7 @@ def producer(instance): return producer -def field_list(relationship_name, field_name): +def related_field_value(relationship_name, field_name): """ Given a relationship name and the name of a field, return a producer which returns a list containing the value of that field for each object in the relationship @@ -49,4 +49,4 @@ def pk_list(relationship_name): just a single PK if this is a to-one field, but this is an inefficient way of doing it). """ - return field_list(relationship_name, "pk") + return related_field_value(relationship_name, "pk") diff --git a/tests/test_pairs.py b/tests/test_pairs.py index 5f29928..a5ad7e5 100644 --- a/tests/test_pairs.py +++ b/tests/test_pairs.py @@ -639,29 +639,30 @@ def test_order_by(self): self.assertEqual(result, [{"name": "a"}, {"name": "b"}, {"name": "c"}]) -class FieldListTestCase(TestCase): - def test_field_list(self): +class RelatedFieldValueTestCase(TestCase): + def test_related_field_value(self): owner = Owner.objects.create(name="test owner") Widget.objects.create(name="test 1", owner=owner) Widget.objects.create(name="test 2", owner=owner) Widget.objects.create(name="test 3", owner=owner) prepare, project = pairs.producer_to_projector( - "widget_set", pairs.field_list("widget_set", "name") + "widget_set", pairs.related_field_value("widget_set", "name") ) queryset = prepare(Owner.objects.all()) result = project(queryset.first()) self.assertEqual(result, {"widget_set": ["test 1", "test 2", "test 3"]}) - def test_field_list_with_to_attr(self): + def test_related_field_value_with_to_attr(self): owner = Owner.objects.create(name="test owner") Widget.objects.create(name="test 1", owner=owner) Widget.objects.create(name="test 2", owner=owner) Widget.objects.create(name="test 3", owner=owner) prepare, project = pairs.producer_to_projector( - "widgets", pairs.field_list("widget_set", "name", to_attr="widgets") + "widgets", + pairs.related_field_value("widget_set", "name", to_attr="widgets"), ) queryset = prepare(Owner.objects.all()) diff --git a/tests/test_producers.py b/tests/test_producers.py index 8f2f143..ac719aa 100644 --- a/tests/test_producers.py +++ b/tests/test_producers.py @@ -156,14 +156,14 @@ def hello(self, name): self.assertEqual(result, "hello, tester!") -class FieldListTestCase(TestCase): - def test_field_list(self): +class RelatedFieldValueTestCase(TestCase): + def test_related_field_value(self): owner = Owner.objects.create(name="test owner") Widget.objects.create(name="test 1", owner=owner) Widget.objects.create(name="test 2", owner=owner) Widget.objects.create(name="test 3", owner=owner) - produce = producers.field_list("widget_set", "name") + produce = producers.related_field_value("widget_set", "name") result = produce(owner) self.assertEqual(result, ["test 1", "test 2", "test 3"]) From e7a409d41516cc4bfeaa844994aefd2047afd97b Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Sat, 29 Jan 2022 21:42:33 +0000 Subject: [PATCH 07/11] Add test for related field value for to-one relationship --- tests/test_pairs.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_pairs.py b/tests/test_pairs.py index a5ad7e5..dfcd219 100644 --- a/tests/test_pairs.py +++ b/tests/test_pairs.py @@ -669,6 +669,18 @@ def test_related_field_value_with_to_attr(self): result = project(queryset.first()) self.assertEqual(result, {"widgets": ["test 1", "test 2", "test 3"]}) + def test_related_field_value_single_object(self): + owner = Owner.objects.create(name="test owner") + Widget.objects.create(name="test widget", owner=owner) + + prepare, project = pairs.producer_to_projector( + "owner_name", pairs.related_field_value("owner", "name") + ) + + queryset = prepare(Widget.objects.all()) + result = project(queryset.first()) + self.assertEqual(result, {"owner_name": "test owner"}) + class PKListTestCase(TestCase): def test_pk_list(self): From 970024190456997f7aeeefbc1d3c905d330eb6c4 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Sat, 29 Jan 2022 21:44:44 +0000 Subject: [PATCH 08/11] Rename related_field_value to related_field --- django_readers/pairs.py | 6 +++--- django_readers/producers.py | 4 ++-- tests/test_pairs.py | 14 +++++++------- tests/test_producers.py | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/django_readers/pairs.py b/django_readers/pairs.py index 10e5236..9d708b5 100644 --- a/django_readers/pairs.py +++ b/django_readers/pairs.py @@ -111,16 +111,16 @@ def relationship(name, relationship_pair, to_attr=None): return prepare, producers.relationship(to_attr or name, project_relationship) -def related_field_value(relationship_name, field_name, to_attr=None): +def related_field(relationship_name, field_name, to_attr=None): return ( qs.auto_prefetch_relationship( relationship_name, qs.include_fields(field_name), to_attr=to_attr, ), - producers.related_field_value(to_attr or relationship_name, field_name), + producers.related_field(to_attr or relationship_name, field_name), ) def pk_list(relationship_name, to_attr=None): - return related_field_value(relationship_name, "pk", to_attr=to_attr) + return related_field(relationship_name, "pk", to_attr=to_attr) diff --git a/django_readers/producers.py b/django_readers/producers.py index 0f73a94..c587401 100644 --- a/django_readers/producers.py +++ b/django_readers/producers.py @@ -34,7 +34,7 @@ def producer(instance): return producer -def related_field_value(relationship_name, field_name): +def related_field(relationship_name, field_name): """ Given a relationship name and the name of a field, return a producer which returns a list containing the value of that field for each object in the relationship @@ -49,4 +49,4 @@ def pk_list(relationship_name): just a single PK if this is a to-one field, but this is an inefficient way of doing it). """ - return related_field_value(relationship_name, "pk") + return related_field(relationship_name, "pk") diff --git a/tests/test_pairs.py b/tests/test_pairs.py index dfcd219..d9f2352 100644 --- a/tests/test_pairs.py +++ b/tests/test_pairs.py @@ -639,22 +639,22 @@ def test_order_by(self): self.assertEqual(result, [{"name": "a"}, {"name": "b"}, {"name": "c"}]) -class RelatedFieldValueTestCase(TestCase): - def test_related_field_value(self): +class RelatedFieldTestCase(TestCase): + def test_related_field(self): owner = Owner.objects.create(name="test owner") Widget.objects.create(name="test 1", owner=owner) Widget.objects.create(name="test 2", owner=owner) Widget.objects.create(name="test 3", owner=owner) prepare, project = pairs.producer_to_projector( - "widget_set", pairs.related_field_value("widget_set", "name") + "widget_set", pairs.related_field("widget_set", "name") ) queryset = prepare(Owner.objects.all()) result = project(queryset.first()) self.assertEqual(result, {"widget_set": ["test 1", "test 2", "test 3"]}) - def test_related_field_value_with_to_attr(self): + def test_related_field_with_to_attr(self): owner = Owner.objects.create(name="test owner") Widget.objects.create(name="test 1", owner=owner) Widget.objects.create(name="test 2", owner=owner) @@ -662,19 +662,19 @@ def test_related_field_value_with_to_attr(self): prepare, project = pairs.producer_to_projector( "widgets", - pairs.related_field_value("widget_set", "name", to_attr="widgets"), + pairs.related_field("widget_set", "name", to_attr="widgets"), ) queryset = prepare(Owner.objects.all()) result = project(queryset.first()) self.assertEqual(result, {"widgets": ["test 1", "test 2", "test 3"]}) - def test_related_field_value_single_object(self): + def test_related_field_single_object(self): owner = Owner.objects.create(name="test owner") Widget.objects.create(name="test widget", owner=owner) prepare, project = pairs.producer_to_projector( - "owner_name", pairs.related_field_value("owner", "name") + "owner_name", pairs.related_field("owner", "name") ) queryset = prepare(Widget.objects.all()) diff --git a/tests/test_producers.py b/tests/test_producers.py index ac719aa..4cc786b 100644 --- a/tests/test_producers.py +++ b/tests/test_producers.py @@ -156,14 +156,14 @@ def hello(self, name): self.assertEqual(result, "hello, tester!") -class RelatedFieldValueTestCase(TestCase): - def test_related_field_value(self): +class RelatedFieldTestCase(TestCase): + def test_related_field(self): owner = Owner.objects.create(name="test owner") Widget.objects.create(name="test 1", owner=owner) Widget.objects.create(name="test 2", owner=owner) Widget.objects.create(name="test 3", owner=owner) - produce = producers.related_field_value("widget_set", "name") + produce = producers.related_field("widget_set", "name") result = produce(owner) self.assertEqual(result, ["test 1", "test 2", "test 3"]) From 30e6b2c7ccf10acc8d1b6278b466be83b2c9a259 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Sat, 29 Jan 2022 22:05:12 +0000 Subject: [PATCH 09/11] Add related_field to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2d870c..ba2ffdb 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ Note that `django-readers` _always_ uses `prefetch_related` to load relationship Of course, it is quite possible to use `select_related` by applying `qs.select_related` at the root of your query, but this must be done manually. `django-readers` also provides `qs.select_related_fields`, which combines `select_related` with `include_fields` to allow you to specify exactly which fields you need from the related objects. -You can use `pairs.pk_list` to produce a list containing just the primary keys of the related objects. +You can use `pairs.pk_list` to produce a list containing just the primary keys of the related objects. A more general form of this function is `pairs.related_field`, which returns the value (or a list of the values) of any field from a related objects or objects. As a shortcut, the `pairs` module provides functions called `filter`, `exclude` and `order_by`, which can be used to apply the given queryset functions to the queryset _without affecting the projection_. These are equivalent to (for example) `(qs.filter(arg=value), projectors.noop)` and are most useful for filtering or ordering related objects: From c5c34c0a4a24069239d35c3a2d9b066feea61c40 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Sat, 29 Jan 2022 22:07:07 +0000 Subject: [PATCH 10/11] Add to related_field to CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abc8c9f..7d8f462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## Added +- New `producers.related_field` and `pairs.related_field` functions for fetching the value of any field from a related object or objects. + ## [1.0.0] - 2020-10-13 Initial stable release. From ab703c763ff15f7893f34cb36edf7a4875538176 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Mon, 21 Feb 2022 20:07:24 +0000 Subject: [PATCH 11/11] Correct heading level in CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d8f462..2f93b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## Added +### Added - New `producers.related_field` and `pairs.related_field` functions for fetching the value of any field from a related object or objects. ## [1.0.0] - 2020-10-13