Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 139 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,138 @@ print(serializer.data)
}
```

## Serializer Method Fields

If you want support for nested `expand`, `omit` and `fields` query args in `SerializerMethodField` fields, use `FlexSerializerMethodField`. This can be usefull for serializing iterables that are not related to your original model. For example:

```python
from rest_framework import serializers
from rest_flex_fields.serializers import FlexFieldsModelSerializer, FlexFieldsSerializerMixin
from rest_flex_fields.fields import FlexSerializerMethodField


class EventSerializer(FlexFieldsSerializerMixin, serializers.Serializer):
name = serializers.CharField()
city = serializers.CharField()
tickets = serializers.CharField()

class Meta:
expandable_fields = {
"city": serializers.SerializerMethodField
}

def get_city(self, obj):
return { "name": obj["city"] }


class CountrySerializer(FlexFieldsModelSerializer):

class Meta:
model = Country
fields = ['name']
expandable_fields = {
"events": FlexSerializerMethodField
}

def get_events(self, obj, fields, expand, omit):
events = get_event_list(country=obj)
return EventSerializer(events, many=True, expand=expand, omit=omit, fields=fields).data

```

Default expand `GET /countries?expand=events`:

```json
[
{
"name": "Germany",
"events": [
{
"name": "Wacken Open Air",
"city": "Wacken",
"tickets": "www.example.com/wacken"
},
{
"name": "Full Force",
"city": "Grafenhainichen",
"tickets": "www.example.com/full_force"
}
]
},
{
"name": "Spain",
"events": [
{
"name": "Resurrection",
"city": "Viveiro",
"tickets": "www.example.com/resurrection"
}
]
}
]
```

You can then use query args to filter the `events` fields. `GET /country_events?expand=events&fields=name,events.name`:

```json
[
{
"name": "Germany",
"events": [
{
"name": "Wacken Open Air"
},
{
"name": "Full Force"
}
]
},
{
"name": "Spain",
"events": [
{
"name": "Resurrection"
}
]
}
]
```

`omit` and `expand` also work, `GET /country_events?expand=events,events.city&omit=events.name`:

```json
[
{
"name": "Germany",
"events": [
{
"city": {
"name": "Wacken"
},
"tickets": "www.example.com/wacken"
},
{
"city": {
"name": "Grafenhainichen"
},
"tickets": "www.example.com/full_force"
}
]
},
{
"name": "Spain",
"events": [
{
"city": {
"name": "Viveiro"
},
"tickets": "www.example.com/resurrection"
}
]
}
]
```

# Serializer Options

Dynamic field options can be passed in the following ways:
Expand Down Expand Up @@ -484,9 +616,9 @@ class PersonSerializer(FlexFieldsModelSerializer):
Parameter names and wildcard values can be configured within a Django setting, named `REST_FLEX_FIELDS`.

| Option | Description | Default |
|-------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|-----------------|
| ----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | --------------- |
| EXPAND_PARAM | The name of the parameter with the fields to be expanded | `"expand"` |
| MAXIMUM_EXPANSION_DEPTH | The max allowed expansion depth. By default it's unlimited. Expanding `state.towns` would equal a depth of 2 | `None` |
| MAXIMUM_EXPANSION_DEPTH | The max allowed expansion depth. By default it's unlimited. Expanding `state.towns` would equal a depth of 2 | `None` |
| FIELDS_PARAM | The name of the parameter with the fields to be included (others will be omitted) | `"fields"` |
| OMIT_PARAM | The name of the parameter with the fields to be omitted | `"omit"` |
| RECURSIVE_EXPANSION_PERMITTED | If `False`, an exception is raised when a recursive pattern is found | `True` |
Expand All @@ -504,8 +636,7 @@ A `maximum_expansion_depth` integer property can be set on a serializer class.

`recursive_expansion_permitted` boolean property can be set on a serializer class.

Both settings raise `serializers.ValidationError` when conditions are met but exceptions can be customized by overriding the `recursive_expansion_not_permitted` and `expansion_depth_exceeded` methods.

Both settings raise `serializers.ValidationError` when conditions are met but exceptions can be customized by overriding the `recursive_expansion_not_permitted` and `expansion_depth_exceeded` methods.

## Serializer Introspection

Expand Down Expand Up @@ -584,6 +715,10 @@ It will automatically call `select_related` and `prefetch_related` on the curren

# Changelog <a id="changelog"></a>

## Unreleased

- Adds `FlexSerializerMethodField`.

## 1.0.2 (March 2023)

- Adds control over whether recursive expansions are allowed and allows setting the max expansion depth. Thanks @andruten!
Expand Down
12 changes: 12 additions & 0 deletions rest_flex_fields/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from rest_framework import serializers

class FlexSerializerMethodField(serializers.SerializerMethodField):
def __init__(self, method_name=None, **kwargs):
self.expand = kwargs.pop("expand", [])
self.fields = kwargs.pop("fields", [])
self.omit = kwargs.pop("omit", [])
super().__init__(method_name, **kwargs)

def to_representation(self, value):
method = getattr(self.parent, self.method_name)
return method(value, self.fields, self.expand, self.omit)
6 changes: 4 additions & 2 deletions rest_flex_fields/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
RECURSIVE_EXPANSION_PERMITTED,
split_levels,
)
from .fields import FlexSerializerMethodField


class FlexFieldsSerializerMixin(object):
Expand Down Expand Up @@ -132,8 +133,9 @@ def _make_expanded_field_serializer(
if issubclass(serializer_class, serializers.Serializer):
settings["context"] = self.context

if issubclass(serializer_class, FlexFieldsSerializerMixin):
settings["parent"] = self
if issubclass(serializer_class, (FlexFieldsSerializerMixin, FlexSerializerMethodField)):
if issubclass(serializer_class, FlexFieldsSerializerMixin):
settings["parent"] = self

if name in nested_expand:
settings[EXPAND_PARAM] = nested_expand[name]
Expand Down
130 changes: 130 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from django.test import TestCase
from django.urls import reverse
from tests.testapp.models import Country


class TestFlexSerializerMethodField(TestCase):
@classmethod
def setUpClass(cls) -> None:
Country.objects.create(name="Germany")
Country.objects.create(name="Spain")
return super().setUpClass()

@classmethod
def tearDownClass(cls) -> None:
Country.objects.all().delete()
return super().tearDownClass()

def setUp(self) -> None:
self.url = reverse("country-events-list")
return super().setUp()

def test_base_request(self):
r = self.client.get(self.url)

self.assertListEqual(
r.json(),
[
{
"name": "Germany",
},
{
"name": "Spain",
}
]
)

def test_omit_arg(self):
r = self.client.get(self.url+"?omit=name,events.tickets&expand=events")
self.assertListEqual(
r.json(),
[
{
"events": [
{
"name": "Wacken Open Air",
"city": "Wacken",
},
{
"name": "Full Force",
"city": "Grafenhainichen",
}
]
},
{
"events": [
{
"name": "Resurrection",
"city": "Viveiro",
}
]
}
]
)

def test_fields_arg(self):
r = self.client.get(self.url+"?fields=name,events.name&expand=events")
self.assertListEqual(
r.json(),
[
{
"name": "Germany",
"events": [
{
"name": "Wacken Open Air"
},
{
"name": "Full Force"
}
]
},
{
"name": "Spain",
"events": [
{
"name": "Resurrection"
}
]
}
]
)

def test_expand_arg(self):
r = self.client.get(self.url+"?expand=events.city")
self.assertListEqual(
r.json(),
[
{
"name": "Germany",
"events": [
{
"name": "Wacken Open Air",
"city": {
"name": "Wacken",
},
"tickets": "www.example.com/wacken"
},
{
"name": "Full Force",
"city": {
"name": "Grafenhainichen",
},
"tickets": "www.example.com/full_force"
}
]
},
{
"name": "Spain",
"events": [
{
"name": "Resurrection",
"city": {
"name": "Viveiro",
},
"tickets": "www.example.com/resurrection"
}
]
}
]
)

1 change: 0 additions & 1 deletion tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from http import HTTPStatus
from pprint import pprint
from unittest.mock import patch

from django.contrib.contenttypes.models import ContentType
Expand Down
6 changes: 5 additions & 1 deletion tests/testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ class TaggedItem(models.Model):
tag = models.SlugField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
content_object = GenericForeignKey('content_type', 'object_id')


class Country(models.Model):
name = models.CharField(max_length=200)
Loading