Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Producer and projector for grabbing the value of a related field #52

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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