diff --git a/CHANGELOG.md b/CHANGELOG.md index abc8c9f..2f93b41 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. 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: diff --git a/django_readers/pairs.py b/django_readers/pairs.py index b4dd5ac..9d708b5 100644 --- a/django_readers/pairs.py +++ b/django_readers/pairs.py @@ -111,8 +111,16 @@ def relationship(name, relationship_pair, to_attr=None): return prepare, producers.relationship(to_attr or name, project_relationship) -def pk_list(name, to_attr=None): +def related_field(relationship_name, field_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), + qs.auto_prefetch_relationship( + relationship_name, + qs.include_fields(field_name), + to_attr=to_attr, + ), + producers.related_field(to_attr or relationship_name, field_name), ) + + +def pk_list(relationship_name, to_attr=None): + return related_field(relationship_name, "pk", to_attr=to_attr) diff --git a/django_readers/producers.py b/django_readers/producers.py index a61a268..c587401 100644 --- a/django_readers/producers.py +++ b/django_readers/producers.py @@ -34,11 +34,19 @@ def producer(instance): return producer -def pk_list(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 + """ + return relationship(relationship_name, attrgetter(field_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 relationship(name, attrgetter("pk")) + return related_field(relationship_name, "pk") diff --git a/tests/test_pairs.py b/tests/test_pairs.py index 9e535ab..d9f2352 100644 --- a/tests/test_pairs.py +++ b/tests/test_pairs.py @@ -639,6 +639,49 @@ def test_order_by(self): self.assertEqual(result, [{"name": "a"}, {"name": "b"}, {"name": "c"}]) +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("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_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.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_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("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): owner = Owner.objects.create(name="test owner") diff --git a/tests/test_producers.py b/tests/test_producers.py index a04897d..4cc786b 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 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("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")