Skip to content
Open
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
14 changes: 11 additions & 3 deletions django_readers/pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
12 changes: 10 additions & 2 deletions django_readers/producers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
43 changes: 43 additions & 0 deletions tests/test_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
12 changes: 12 additions & 0 deletions tests/test_producers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down