././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1678541408.2943482
drf-flex-fields-1.0.2/ 0000775 0001750 0001750 00000000000 14403101140 014067 5 ustar 00robert robert ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/LICENSE 0000664 0001750 0001750 00000002052 14400315017 015102 0 ustar 00robert robert MIT License
Copyright (c) 2016 rsinger86
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/MANIFEST.in 0000664 0001750 0001750 00000000122 14400315017 015627 0 ustar 00robert robert include LICENSE
include README.md
include README.txt
recursive-include tests *.py
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1678541408.2943482
drf-flex-fields-1.0.2/PKG-INFO 0000664 0001750 0001750 00000065336 14403101140 015201 0 ustar 00robert robert Metadata-Version: 2.1
Name: drf-flex-fields
Version: 1.0.2
Summary: Flexible, dynamic fields and nested resources for Django REST Framework serializers.
Home-page: https://github.com/rsinger86/drf-flex-fields
Author: Robert Singer
Author-email: robertgsinger@gmail.com
License: MIT
Keywords: django rest api dynamic fields
Platform: UNKNOWN
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Description-Content-Type: text/markdown
License-File: LICENSE
# Django REST - FlexFields
[](https://pypi.python.org/pypi/drf-flex-fields)
[](https://img.shields.io/pypi/status/django-lifecycle.svg/)
Flexible, dynamic fields and nested models for Django REST Framework serializers.
# Overview
FlexFields (DRF-FF) for [Django REST Framework](https://django-rest-framework.org) is a package designed to provide a common baseline of functionality for dynamically setting fields and nested models within DRF serializers. This package is designed for simplicity, with minimal magic and entanglement with DRF's foundational classes.
Key benefits:
- Easily set up fields that be expanded to their fully serialized counterparts via query parameters (`users/?expand=organization,friends`)
- Select a subset of fields by either:
- specifying which ones should be included (`users/?fields=id,first_name`)
- specifying which ones should be excluded (`users/?omit=id,first_name`)
- Use dot notation to dynamically modify fields at arbitrary depths (`users/?expand=organization.owner.roles`)
- Flexible API - options can also be passed directly to a serializer: `UserSerializer(obj, expand=['organization'])`
# Quick Start
```python
from rest_flex_fields import FlexFieldsModelSerializer
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ('id', 'name')
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name', 'population', 'states')
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
class PersonSerializer(FlexFieldsModelSerializer):
class Meta:
model = Person
fields = ('id', 'name', 'country', 'occupation')
expandable_fields = {'country': CountrySerializer}
```
```
GET /people/142/
```
```json
{
"id": 142,
"name": "Jim Halpert",
"country": 1
}
```
```
GET /people/142/?expand=country.states
```
```json
{
"id": 142,
"name": "Jim Halpert",
"country": {
"id": 1,
"name": "United States",
"states": [
{
"id": 23,
"name": "Ohio"
},
{
"id": 2,
"name": "Pennsylvania"
}
]
}
}
```
# Table of Contents:
- [Django REST - FlexFields](#django-rest---flexfields)
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Table of Contents:](#table-of-contents)
- [Setup](#setup)
- [Usage](#usage)
- [Dynamic Field Expansion](#dynamic-field-expansion)
- [Deferred Fields](#deferred-fields)
- [Deep, Nested Expansion](#deep-nested-expansion)
- [Field Expansion on "List" Views ](#field-expansion-on-list-views-)
- [Expanding a "Many" Relationship ](#expanding-a-many-relationship-)
- [Dynamically Setting Fields (Sparse Fields) ](#dynamically-setting-fields-sparse-fields-)
- [Reference serializer as a string (lazy evaluation) ](#reference-serializer-as-a-string-lazy-evaluation-)
- [Increased re-usability of serializers ](#increased-re-usability-of-serializers-)
- [Serializer Options](#serializer-options)
- [Advanced](#advanced)
- [Customization](#customization)
- [Serializer Introspection](#serializer-introspection)
- [Use Wildcards to Match Multiple Fields](#wildcards)
- [Combining Sparse Fields and Field Expansion ](#combining-sparse-fields-and-field-expansion-)
- [Utility Functions ](#utility-functions-)
- [rest_flex_fields.is_expanded(request, field: str)](#rest_flex_fieldsis_expandedrequest-field-str)
- [rest_flex_fields.is_included(request, field: str)](#rest_flex_fieldsis_includedrequest-field-str)
- [Query optimization (experimental)](#query-optimization-experimental)
- [Changelog ](#changelog-)
- [Testing](#testing)
- [License](#license)
# Setup
First install:
```
pip install drf-flex-fields
```
Then have your serializers subclass `FlexFieldsModelSerializer`:
```python
from rest_flex_fields import FlexFieldsModelSerializer
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name')
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name', 'population', 'states')
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
```
Alternatively, you can add the `FlexFieldsSerializerMixin` mixin to a model serializer.
# Usage
## Dynamic Field Expansion
To define expandable fields, add an `expandable_fields` dictionary to your serializer's `Meta` class. Key the dictionary with the name of the field that you want to dynamically expand, and set its value to either the expanded serializer or a tuple where the first element is the serializer and the second is a dictionary of options that will be used to instantiate the serializer.
```python
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': CountrySerializer
}
```
If the default serialized response is the following:
```json
{
"id": 13322,
"name": "John Doe",
"country": 12,
"occupation": "Programmer"
}
```
When you do a `GET /person/13322?expand=country`, the response will change to:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
"population": 330000000
},
"occupation": "Programmer"
}
```
## Deferred Fields
Alternatively, you could treat `country` as a "deferred" field by not defining it among the default fields. To make a field deferred, only define it within the serializer's `expandable_fields`.
## Deep, Nested Expansion
Let's say you add `StateSerializer` as a serializer nested inside the country serializer above:
```python
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ['name', 'population']
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': CountrySerializer
}
```
Your default serialized response might be the following for `person` and `country`, respectively:
```json
{
"id" : 13322,
"name" : "John Doe",
"country" : 12,
"occupation" : "Programmer",
}
{
"id" : 12,
"name" : "United States",
"states" : "http://www.api.com/countries/12/states"
}
```
But if you do a `GET /person/13322?expand=country.states`, it would be:
```json
{
"id": 13322,
"name": "John Doe",
"occupation": "Programmer",
"country": {
"id": 12,
"name": "United States",
"states": [
{
"name": "Ohio",
"population": 11000000
}
]
}
}
```
Please be kind to your database, as this could incur many additional queries. Though, you can mitigate this impact through judicious use of `prefetch_related` and `select_related` when defining the queryset for your viewset.
## Field Expansion on "List" Views
If you request many objects, expanding fields could lead to many additional database queries. Subclass `FlexFieldsModelViewSet` if you want to prevent expanding fields by default when calling a ViewSet's `list` method. Place those fields that you would like to expand in a `permit_list_expands` property on the ViewSet:
```python
from rest_flex_fields import is_expanded
class PersonViewSet(FlexFieldsModelViewSet):
permit_list_expands = ['employer']
serializer_class = PersonSerializer
def get_queryset(self):
queryset = models.Person.objects.all()
if is_expanded(self.request, 'employer'):
queryset = queryset.select_related('employer')
return queryset
```
Notice how this example is using the `is_expanded` utility method as well as `select_related` and `prefetch_related` to efficiently query the database if the field is expanded.
## Expanding a "Many" Relationship
Set `many` to `True` in the serializer options to make sure "to many" fields are expanded correctly.
```python
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ['name', 'population']
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
```
A request to `GET /countries?expand=states` will return:
```python
{
"id" : 12,
"name" : "United States",
"states" : [
{
"name" : "Alabama",
"population": 11000000
},
//... more states ... //
{
"name" : "Ohio",
"population": 11000000
}
]
}
```
## Dynamically Setting Fields (Sparse Fields)
You can use either the `fields` or `omit` keywords to declare only the fields you want to include or to specify fields that should be excluded.
Consider this as a default serialized response:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
"population": 330000000
},
"occupation": "Programmer",
"hobbies": ["rock climbing", "sipping coffee"]
}
```
To whittle down the fields via URL parameters, simply add `?fields=id,name,country` to your requests to get back:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
"population": 330000000
}
}
```
Or, for more specificity, you can use dot-notation, `?fields=id,name,country.name`:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States"
}
}
```
Or, if you want to leave out the nested country object, do `?omit=country`:
```json
{
"id": 13322,
"name": "John Doe",
"occupation": "Programmer",
"hobbies": ["rock climbing", "sipping coffee"]
}
```
## Reference serializer as a string (lazy evaluation)
To avoid circular import problems, it's possible to lazily evaluate a string reference to you serializer class using this syntax:
```python
expandable_fields = {
'record_set': ('.RelatedSerializer', {'many': True})
}
```
**Note**:
Prior to version `0.9.0`, it was assumed your serializer classes would be in a module with the following path:
`.serializers`.
This import style will still work, but you can also now specify fully-qualified import paths to any locations.
## Increased re-usability of serializers
The `omit` and `fields` options can be passed directly to serializers. Rather than defining a separate, slimmer version of a regular serializer, you can re-use the same serializer and declare which fields you want.
```python
from rest_flex_fields import FlexFieldsModelSerializer
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['id', 'name', 'population', 'capital', 'square_miles']
class PersonSerializer(FlexFieldsModelSerializer):
country = CountrySerializer(fields=['id', 'name'])
class Meta:
model = Person
fields = ['id', 'name', 'country']
serializer = PersonSerializer(person)
print(serializer.data)
>>>{
"id": 13322,
"name": "John Doe",
"country": {
"id": 1,
"name": "United States",
}
}
```
# Serializer Options
Dynamic field options can be passed in the following ways:
- from the request's query parameters; separate multiple values with a commma
- as keyword arguments directly to the serializer class when its constructed
- from a dictionary placed as the second element in a tuple when defining `expandable_fields`
Approach #1
```
GET /people?expand=friends.hobbies,employer&omit=age
```
Approach #2
```python
serializer = PersonSerializer(
person,
expand=["friends.hobbies", "employer"],
omit="friends.age"
)
```
Approach #3
```python
class PersonSerializer(FlexFieldsModelSerializer):
// Your field definitions
class Meta:
model = Person
fields = ["age", "hobbies", "name"]
expandable_fields = {
'friends': (
'serializer.FriendSerializer',
{'many': True, "expand": ["hobbies"], "omit": ["age"]}
)
}
```
| Option | Description |
| ------ | :--------------------------------------------------------------------------: |
| expand | Fields to expand; must be configured in the serializer's `expandable_fields` |
| fields | Fields that should be included; all others will be excluded |
| omit | Fields that should be excluded; all others will be included |
# Advanced
## Customization
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` |
| 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` |
| WILDCARD_VALUES | List of values that stand in for all field names. Can be used with the `fields` and `expand` parameters.
When used with `expand`, a wildcard value will trigger the expansion of all `expandable_fields` at a given level.
When used with `fields`, all fields are included at a given level. For example, you could pass `fields=name,state.*` if you have a city resource with a nested state in order to expand only the city's name field and all of the state's fields.
To disable use of wildcards, set this setting to `None`. | `["*", "~all"]` |
For example, if you want your API to work a bit more like [JSON API](https://jsonapi.org/format/#fetching-includes), you could do:
```python
REST_FLEX_FIELDS = {"EXPAND_PARAM": "include"}
```
### Defining Expansion and Recursive Limits on Serializer Classes
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.
## Serializer Introspection
When using an instance of `FlexFieldsModelSerializer`, you can examine the property `expanded_fields` to discover which fields, if any, have been dynamically expanded.
## Use of Wildcard to Match All Fields
You can pass `expand=*` ([or another value of your choosing](#customization)) to automatically expand all fields that are available for expansion at a given level. To refer to nested resources, you can use dot-notation. For example, requesting `expand=menu.sections` for a restaurant resource would expand its nested `menu` resource, as well as that menu's nested `sections` resource.
Or, when requesting sparse fields, you can pass `fields=*` to include only the specified fields at a given level. To refer to nested resources, you can use dot-notation. For example, if you have an `order` resource, you could request all of its fields as well as only two fields on its nested `restaurant` resource with the following: `fields=*,restaurent.name,restaurant.address&expand=restaurant`.
## Combining Sparse Fields and Field Expansion
You may be wondering how things work if you use both the `expand` and `fields` option, and there is overlap. For example, your serialized person model may look like the following by default:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States"
}
}
```
However, you make the following request `HTTP GET /person/13322?include=id,name&expand=country`. You will get the following back:
```json
{
"id": 13322,
"name": "John Doe"
}
```
The `fields` parameter takes precedence over `expand`. That is, if a field is not among the set that is explicitly alllowed, it cannot be expanded. If such a conflict occurs, you will not pay for the extra database queries - the expanded field will be silently abandoned.
## Utility Functions
### rest_flex_fields.is_expanded(request, field: str)
Checks whether a field has been expanded via the request's query parameters.
**Parameters**
- **request**: The request object
- **field**: The name of the field to check
### rest_flex_fields.is_included(request, field: str)
Checks whether a field has NOT been excluded via either the `omit` parameter or the `fields` parameter.
**Parameters**
- **request**: The request object
- **field**: The name of the field to check
## Query optimization (experimental)
An experimental filter backend is available to help you automatically reduce the number of SQL queries and their transfer size. _This feature has not been tested thorougly and any help testing and reporting bugs is greatly appreciated._ You can add FlexFieldFilterBackend to `DEFAULT_FILTER_BACKENDS` in the settings:
```python
# settings.py
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'rest_flex_fields.filter_backends.FlexFieldsFilterBackend',
# ...
),
# ...
}
```
It will automatically call `select_related` and `prefetch_related` on the current QuerySet by determining which fields are needed from many-to-many and foreign key-related models. For sparse fields requests (`?omit=fieldX,fieldY` or `?fields=fieldX,fieldY`), the backend will automatically call `only(*field_names)` using only the fields needed for serialization.
**WARNING:** The optimization currently works only for one nesting level.
# Changelog
## 1.0.2 (March 2023)
- Adds control over whether recursive expansions are allowed and allows setting the max expansion depth. Thanks @andruten!
## 1.0.1 (March 2023)
- Various bug fixes. Thanks @michaelschem, @andruten, and @erielias!
## 1.0.0 (August 2022)
- Improvements to the filter backends for generic foreign key handling and docs generation. Thanks @KrYpTeD974 and @michaelschem!
## 0.9.9 (July 2022)
- Fixes bug in `FlexFieldsFilterBackend`. Thanks @michaelschem!
- Adds `FlexFieldsDocsFilterBackend` for schema population. Thanks @Rjevski!
## 0.9.8 (April 2022)
- Set expandable fields as the default example for expand query parameters in `coreapi.Field`. Thanks @JasperSui!
## 0.9.7 (January 2022)
- Includes m2m in prefetch_related clause even if they're not expanded. Thanks @pablolmedorado and @ADR-007!
## 0.9.6 (November 2021)
- Make it possible to use wildcard values with sparse fields requests.
## 0.9.5 (October 2021)
- Adds OpenAPI support. Thanks @soroush-tabesh!
- Updates tests for Django 3.2 and fixes deprecation warning. Thanks @giovannicimolin!
## 0.9.3 (August 2021)
- Fixes bug where custom parameter names were not passed when constructing nested serializers. Thanks @Kandeel4411!
## 0.9.2 (June 2021)
- Ensures `context` dict is passed down to expanded serializers. Thanks @nikeshyad!
## 0.9.1 (June 2021)
- No longer auto removes `source` argument if it's equal to the field name.
## 0.9.0 (April 2021)
- Allows fully qualified import strings for lazy serializer classes.
## 0.8.9 (February 2021)
- Adds OpenAPI support to experimental filter backend. Thanks @LukasBerka!
## 0.8.8 (September 2020)
- Django 3.1.1 fix. Thansks @NiyazNz!
- Docs typo fix. Thanks @zakjholt!
## 0.8.6 (September 2020)
- Adds `is_included` utility function.
## 0.8.5 (May 2020)
- Adds options to customize parameter names and wildcard values. Closes #10.
## 0.8.1 (May 2020)
- Fixes #44, related to the experimental filter backend. Thanks @jsatt!
## 0.8.0 (April 2020)
- Adds support for `expand`, `omit` and `fields` query parameters for non-GET requests.
- The common use case is creating/updating a model instance and returning a serialized response with expanded fields
- Thanks @kotepillar for raising the issue (#25) and @Crocmagnon for the idea of delaying field modification to `to_representation()`.
## 0.7.5 (February 2020)
- Simplifies declaration of `expandable_fields`
- If using a tuple, the second element - to define the serializer settings - is now optional.
- Instead of a tuple, you can now just use the serializer class or a string to lazily reference that class.
- Updates documentation.
## 0.7.0 (February 2020)
- Adds support for different ways of passing arrays in query strings. Thanks @sentyaev!
- Fixes attribute error when map is supplied to split levels utility function. Thanks @hemache!
## 0.6.1 (September 2019)
- Adds experimental support for automatically SQL query optimization via a `FlexFieldsFilterBackend`. Thanks ADR-007!
- Adds CircleCI config file. Thanks mikeIFTS!
- Moves declaration of `expandable_fields` to `Meta` class on serialzer for consistency with DRF (will continue to support declaration as class property)
- Python 2 is no longer supported. If you need Python 2 support, you can continue to use older versions of this package.
## 0.5.0 (April 2019)
- Added support for `omit` keyword for field exclusion. Code clean up and improved test coverage.
## 0.3.4 (May 2018)
- Handle case where `request` is `None` when accessing request object from serializer. Thanks @jsatt!
## 0.3.3 (April 2018)
- Exposes `FlexFieldsSerializerMixin` in addition to `FlexFieldsModelSerializer`. Thanks @jsatt!
# Testing
Tests are found in a simplified DRF project in the `/tests` folder. Install the project requirements and do `./manage.py test` to run them.
# License
See [License](LICENSE.md).
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678541268.0
drf-flex-fields-1.0.2/README.md 0000664 0001750 0001750 00000063607 14403100724 015371 0 ustar 00robert robert # Django REST - FlexFields
[](https://pypi.python.org/pypi/drf-flex-fields)
[](https://img.shields.io/pypi/status/django-lifecycle.svg/)
Flexible, dynamic fields and nested models for Django REST Framework serializers.
# Overview
FlexFields (DRF-FF) for [Django REST Framework](https://django-rest-framework.org) is a package designed to provide a common baseline of functionality for dynamically setting fields and nested models within DRF serializers. This package is designed for simplicity, with minimal magic and entanglement with DRF's foundational classes.
Key benefits:
- Easily set up fields that be expanded to their fully serialized counterparts via query parameters (`users/?expand=organization,friends`)
- Select a subset of fields by either:
- specifying which ones should be included (`users/?fields=id,first_name`)
- specifying which ones should be excluded (`users/?omit=id,first_name`)
- Use dot notation to dynamically modify fields at arbitrary depths (`users/?expand=organization.owner.roles`)
- Flexible API - options can also be passed directly to a serializer: `UserSerializer(obj, expand=['organization'])`
# Quick Start
```python
from rest_flex_fields import FlexFieldsModelSerializer
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ('id', 'name')
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name', 'population', 'states')
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
class PersonSerializer(FlexFieldsModelSerializer):
class Meta:
model = Person
fields = ('id', 'name', 'country', 'occupation')
expandable_fields = {'country': CountrySerializer}
```
```
GET /people/142/
```
```json
{
"id": 142,
"name": "Jim Halpert",
"country": 1
}
```
```
GET /people/142/?expand=country.states
```
```json
{
"id": 142,
"name": "Jim Halpert",
"country": {
"id": 1,
"name": "United States",
"states": [
{
"id": 23,
"name": "Ohio"
},
{
"id": 2,
"name": "Pennsylvania"
}
]
}
}
```
# Table of Contents:
- [Django REST - FlexFields](#django-rest---flexfields)
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Table of Contents:](#table-of-contents)
- [Setup](#setup)
- [Usage](#usage)
- [Dynamic Field Expansion](#dynamic-field-expansion)
- [Deferred Fields](#deferred-fields)
- [Deep, Nested Expansion](#deep-nested-expansion)
- [Field Expansion on "List" Views ](#field-expansion-on-list-views-)
- [Expanding a "Many" Relationship ](#expanding-a-many-relationship-)
- [Dynamically Setting Fields (Sparse Fields) ](#dynamically-setting-fields-sparse-fields-)
- [Reference serializer as a string (lazy evaluation) ](#reference-serializer-as-a-string-lazy-evaluation-)
- [Increased re-usability of serializers ](#increased-re-usability-of-serializers-)
- [Serializer Options](#serializer-options)
- [Advanced](#advanced)
- [Customization](#customization)
- [Serializer Introspection](#serializer-introspection)
- [Use Wildcards to Match Multiple Fields](#wildcards)
- [Combining Sparse Fields and Field Expansion ](#combining-sparse-fields-and-field-expansion-)
- [Utility Functions ](#utility-functions-)
- [rest_flex_fields.is_expanded(request, field: str)](#rest_flex_fieldsis_expandedrequest-field-str)
- [rest_flex_fields.is_included(request, field: str)](#rest_flex_fieldsis_includedrequest-field-str)
- [Query optimization (experimental)](#query-optimization-experimental)
- [Changelog ](#changelog-)
- [Testing](#testing)
- [License](#license)
# Setup
First install:
```
pip install drf-flex-fields
```
Then have your serializers subclass `FlexFieldsModelSerializer`:
```python
from rest_flex_fields import FlexFieldsModelSerializer
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name')
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name', 'population', 'states')
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
```
Alternatively, you can add the `FlexFieldsSerializerMixin` mixin to a model serializer.
# Usage
## Dynamic Field Expansion
To define expandable fields, add an `expandable_fields` dictionary to your serializer's `Meta` class. Key the dictionary with the name of the field that you want to dynamically expand, and set its value to either the expanded serializer or a tuple where the first element is the serializer and the second is a dictionary of options that will be used to instantiate the serializer.
```python
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': CountrySerializer
}
```
If the default serialized response is the following:
```json
{
"id": 13322,
"name": "John Doe",
"country": 12,
"occupation": "Programmer"
}
```
When you do a `GET /person/13322?expand=country`, the response will change to:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
"population": 330000000
},
"occupation": "Programmer"
}
```
## Deferred Fields
Alternatively, you could treat `country` as a "deferred" field by not defining it among the default fields. To make a field deferred, only define it within the serializer's `expandable_fields`.
## Deep, Nested Expansion
Let's say you add `StateSerializer` as a serializer nested inside the country serializer above:
```python
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ['name', 'population']
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': CountrySerializer
}
```
Your default serialized response might be the following for `person` and `country`, respectively:
```json
{
"id" : 13322,
"name" : "John Doe",
"country" : 12,
"occupation" : "Programmer",
}
{
"id" : 12,
"name" : "United States",
"states" : "http://www.api.com/countries/12/states"
}
```
But if you do a `GET /person/13322?expand=country.states`, it would be:
```json
{
"id": 13322,
"name": "John Doe",
"occupation": "Programmer",
"country": {
"id": 12,
"name": "United States",
"states": [
{
"name": "Ohio",
"population": 11000000
}
]
}
}
```
Please be kind to your database, as this could incur many additional queries. Though, you can mitigate this impact through judicious use of `prefetch_related` and `select_related` when defining the queryset for your viewset.
## Field Expansion on "List" Views
If you request many objects, expanding fields could lead to many additional database queries. Subclass `FlexFieldsModelViewSet` if you want to prevent expanding fields by default when calling a ViewSet's `list` method. Place those fields that you would like to expand in a `permit_list_expands` property on the ViewSet:
```python
from rest_flex_fields import is_expanded
class PersonViewSet(FlexFieldsModelViewSet):
permit_list_expands = ['employer']
serializer_class = PersonSerializer
def get_queryset(self):
queryset = models.Person.objects.all()
if is_expanded(self.request, 'employer'):
queryset = queryset.select_related('employer')
return queryset
```
Notice how this example is using the `is_expanded` utility method as well as `select_related` and `prefetch_related` to efficiently query the database if the field is expanded.
## Expanding a "Many" Relationship
Set `many` to `True` in the serializer options to make sure "to many" fields are expanded correctly.
```python
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ['name', 'population']
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
```
A request to `GET /countries?expand=states` will return:
```python
{
"id" : 12,
"name" : "United States",
"states" : [
{
"name" : "Alabama",
"population": 11000000
},
//... more states ... //
{
"name" : "Ohio",
"population": 11000000
}
]
}
```
## Dynamically Setting Fields (Sparse Fields)
You can use either the `fields` or `omit` keywords to declare only the fields you want to include or to specify fields that should be excluded.
Consider this as a default serialized response:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
"population": 330000000
},
"occupation": "Programmer",
"hobbies": ["rock climbing", "sipping coffee"]
}
```
To whittle down the fields via URL parameters, simply add `?fields=id,name,country` to your requests to get back:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
"population": 330000000
}
}
```
Or, for more specificity, you can use dot-notation, `?fields=id,name,country.name`:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States"
}
}
```
Or, if you want to leave out the nested country object, do `?omit=country`:
```json
{
"id": 13322,
"name": "John Doe",
"occupation": "Programmer",
"hobbies": ["rock climbing", "sipping coffee"]
}
```
## Reference serializer as a string (lazy evaluation)
To avoid circular import problems, it's possible to lazily evaluate a string reference to you serializer class using this syntax:
```python
expandable_fields = {
'record_set': ('.RelatedSerializer', {'many': True})
}
```
**Note**:
Prior to version `0.9.0`, it was assumed your serializer classes would be in a module with the following path:
`.serializers`.
This import style will still work, but you can also now specify fully-qualified import paths to any locations.
## Increased re-usability of serializers
The `omit` and `fields` options can be passed directly to serializers. Rather than defining a separate, slimmer version of a regular serializer, you can re-use the same serializer and declare which fields you want.
```python
from rest_flex_fields import FlexFieldsModelSerializer
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['id', 'name', 'population', 'capital', 'square_miles']
class PersonSerializer(FlexFieldsModelSerializer):
country = CountrySerializer(fields=['id', 'name'])
class Meta:
model = Person
fields = ['id', 'name', 'country']
serializer = PersonSerializer(person)
print(serializer.data)
>>>{
"id": 13322,
"name": "John Doe",
"country": {
"id": 1,
"name": "United States",
}
}
```
# Serializer Options
Dynamic field options can be passed in the following ways:
- from the request's query parameters; separate multiple values with a commma
- as keyword arguments directly to the serializer class when its constructed
- from a dictionary placed as the second element in a tuple when defining `expandable_fields`
Approach #1
```
GET /people?expand=friends.hobbies,employer&omit=age
```
Approach #2
```python
serializer = PersonSerializer(
person,
expand=["friends.hobbies", "employer"],
omit="friends.age"
)
```
Approach #3
```python
class PersonSerializer(FlexFieldsModelSerializer):
// Your field definitions
class Meta:
model = Person
fields = ["age", "hobbies", "name"]
expandable_fields = {
'friends': (
'serializer.FriendSerializer',
{'many': True, "expand": ["hobbies"], "omit": ["age"]}
)
}
```
| Option | Description |
| ------ | :--------------------------------------------------------------------------: |
| expand | Fields to expand; must be configured in the serializer's `expandable_fields` |
| fields | Fields that should be included; all others will be excluded |
| omit | Fields that should be excluded; all others will be included |
# Advanced
## Customization
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` |
| 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` |
| WILDCARD_VALUES | List of values that stand in for all field names. Can be used with the `fields` and `expand` parameters.
When used with `expand`, a wildcard value will trigger the expansion of all `expandable_fields` at a given level.
When used with `fields`, all fields are included at a given level. For example, you could pass `fields=name,state.*` if you have a city resource with a nested state in order to expand only the city's name field and all of the state's fields.
To disable use of wildcards, set this setting to `None`. | `["*", "~all"]` |
For example, if you want your API to work a bit more like [JSON API](https://jsonapi.org/format/#fetching-includes), you could do:
```python
REST_FLEX_FIELDS = {"EXPAND_PARAM": "include"}
```
### Defining Expansion and Recursive Limits on Serializer Classes
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.
## Serializer Introspection
When using an instance of `FlexFieldsModelSerializer`, you can examine the property `expanded_fields` to discover which fields, if any, have been dynamically expanded.
## Use of Wildcard to Match All Fields
You can pass `expand=*` ([or another value of your choosing](#customization)) to automatically expand all fields that are available for expansion at a given level. To refer to nested resources, you can use dot-notation. For example, requesting `expand=menu.sections` for a restaurant resource would expand its nested `menu` resource, as well as that menu's nested `sections` resource.
Or, when requesting sparse fields, you can pass `fields=*` to include only the specified fields at a given level. To refer to nested resources, you can use dot-notation. For example, if you have an `order` resource, you could request all of its fields as well as only two fields on its nested `restaurant` resource with the following: `fields=*,restaurent.name,restaurant.address&expand=restaurant`.
## Combining Sparse Fields and Field Expansion
You may be wondering how things work if you use both the `expand` and `fields` option, and there is overlap. For example, your serialized person model may look like the following by default:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States"
}
}
```
However, you make the following request `HTTP GET /person/13322?include=id,name&expand=country`. You will get the following back:
```json
{
"id": 13322,
"name": "John Doe"
}
```
The `fields` parameter takes precedence over `expand`. That is, if a field is not among the set that is explicitly alllowed, it cannot be expanded. If such a conflict occurs, you will not pay for the extra database queries - the expanded field will be silently abandoned.
## Utility Functions
### rest_flex_fields.is_expanded(request, field: str)
Checks whether a field has been expanded via the request's query parameters.
**Parameters**
- **request**: The request object
- **field**: The name of the field to check
### rest_flex_fields.is_included(request, field: str)
Checks whether a field has NOT been excluded via either the `omit` parameter or the `fields` parameter.
**Parameters**
- **request**: The request object
- **field**: The name of the field to check
## Query optimization (experimental)
An experimental filter backend is available to help you automatically reduce the number of SQL queries and their transfer size. _This feature has not been tested thorougly and any help testing and reporting bugs is greatly appreciated._ You can add FlexFieldFilterBackend to `DEFAULT_FILTER_BACKENDS` in the settings:
```python
# settings.py
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'rest_flex_fields.filter_backends.FlexFieldsFilterBackend',
# ...
),
# ...
}
```
It will automatically call `select_related` and `prefetch_related` on the current QuerySet by determining which fields are needed from many-to-many and foreign key-related models. For sparse fields requests (`?omit=fieldX,fieldY` or `?fields=fieldX,fieldY`), the backend will automatically call `only(*field_names)` using only the fields needed for serialization.
**WARNING:** The optimization currently works only for one nesting level.
# Changelog
## 1.0.2 (March 2023)
- Adds control over whether recursive expansions are allowed and allows setting the max expansion depth. Thanks @andruten!
## 1.0.1 (March 2023)
- Various bug fixes. Thanks @michaelschem, @andruten, and @erielias!
## 1.0.0 (August 2022)
- Improvements to the filter backends for generic foreign key handling and docs generation. Thanks @KrYpTeD974 and @michaelschem!
## 0.9.9 (July 2022)
- Fixes bug in `FlexFieldsFilterBackend`. Thanks @michaelschem!
- Adds `FlexFieldsDocsFilterBackend` for schema population. Thanks @Rjevski!
## 0.9.8 (April 2022)
- Set expandable fields as the default example for expand query parameters in `coreapi.Field`. Thanks @JasperSui!
## 0.9.7 (January 2022)
- Includes m2m in prefetch_related clause even if they're not expanded. Thanks @pablolmedorado and @ADR-007!
## 0.9.6 (November 2021)
- Make it possible to use wildcard values with sparse fields requests.
## 0.9.5 (October 2021)
- Adds OpenAPI support. Thanks @soroush-tabesh!
- Updates tests for Django 3.2 and fixes deprecation warning. Thanks @giovannicimolin!
## 0.9.3 (August 2021)
- Fixes bug where custom parameter names were not passed when constructing nested serializers. Thanks @Kandeel4411!
## 0.9.2 (June 2021)
- Ensures `context` dict is passed down to expanded serializers. Thanks @nikeshyad!
## 0.9.1 (June 2021)
- No longer auto removes `source` argument if it's equal to the field name.
## 0.9.0 (April 2021)
- Allows fully qualified import strings for lazy serializer classes.
## 0.8.9 (February 2021)
- Adds OpenAPI support to experimental filter backend. Thanks @LukasBerka!
## 0.8.8 (September 2020)
- Django 3.1.1 fix. Thansks @NiyazNz!
- Docs typo fix. Thanks @zakjholt!
## 0.8.6 (September 2020)
- Adds `is_included` utility function.
## 0.8.5 (May 2020)
- Adds options to customize parameter names and wildcard values. Closes #10.
## 0.8.1 (May 2020)
- Fixes #44, related to the experimental filter backend. Thanks @jsatt!
## 0.8.0 (April 2020)
- Adds support for `expand`, `omit` and `fields` query parameters for non-GET requests.
- The common use case is creating/updating a model instance and returning a serialized response with expanded fields
- Thanks @kotepillar for raising the issue (#25) and @Crocmagnon for the idea of delaying field modification to `to_representation()`.
## 0.7.5 (February 2020)
- Simplifies declaration of `expandable_fields`
- If using a tuple, the second element - to define the serializer settings - is now optional.
- Instead of a tuple, you can now just use the serializer class or a string to lazily reference that class.
- Updates documentation.
## 0.7.0 (February 2020)
- Adds support for different ways of passing arrays in query strings. Thanks @sentyaev!
- Fixes attribute error when map is supplied to split levels utility function. Thanks @hemache!
## 0.6.1 (September 2019)
- Adds experimental support for automatically SQL query optimization via a `FlexFieldsFilterBackend`. Thanks ADR-007!
- Adds CircleCI config file. Thanks mikeIFTS!
- Moves declaration of `expandable_fields` to `Meta` class on serialzer for consistency with DRF (will continue to support declaration as class property)
- Python 2 is no longer supported. If you need Python 2 support, you can continue to use older versions of this package.
## 0.5.0 (April 2019)
- Added support for `omit` keyword for field exclusion. Code clean up and improved test coverage.
## 0.3.4 (May 2018)
- Handle case where `request` is `None` when accessing request object from serializer. Thanks @jsatt!
## 0.3.3 (April 2018)
- Exposes `FlexFieldsSerializerMixin` in addition to `FlexFieldsModelSerializer`. Thanks @jsatt!
# Testing
Tests are found in a simplified DRF project in the `/tests` folder. Install the project requirements and do `./manage.py test` to run them.
# License
See [License](LICENSE.md).
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/README.txt 0000664 0001750 0001750 00000040260 14400315017 015576 0 ustar 00robert robert Django REST - FlexFields
========================
Flexible, dynamic fields and nested models for Django REST Framework
serializers. Works with both Python 2 and 3.
Overview
========
FlexFields (DRF-FF) for `Django REST
Framework `__ is a package designed
to provide a common baseline of functionality for dynamically setting
fields and nested models within DRF serializers. To remove unneeded
fields, you can dynamically set fields, including nested fields, via URL
parameters ``(?fields=name,address.zip)`` or when configuring
serializers. Additionally, you can dynamically expand fields from simple
values to complex nested models, or treat fields as "deferred", and
expand them on an as-needed basis.
This package is designed for simplicity and provides two classes - a
viewset class and a serializer class (or mixin) - with minimal magic and
entanglement with DRF's foundational classes. Unless DRF makes
significant changes to its serializers, you can count on this package to
work (and if major changes are made, this package will be updated
shortly thereafter). If you are familar with Django REST Framework, it
shouldn't take you long to read over the code and see how it works.
There are similar packages, such as the powerful `Dynamic
REST `__, which does what
this package does and more, but you may not need all those bells and
whistles. There is also the more basic `Dynamic Fields
Mixin `__, but it lacks
functionality for field expansion and dot-notation field customiziation.
Table of Contents:
- `Installation <#installation>`__
- `Requirements <#requirements>`__
- `Basics <#basics>`__
- `Dynamic Field Expansion <#dynamic-field-expansion>`__
- `Deferred Fields <#deferred-fields>`__
- `Deep, Nested Expansion <#deep-nested-expansion>`__
- `Configuration from Serializer
Options <#configuration-from-serializer-options>`__
- `Field Expansion on "List" Views <#field-expansion-on-list-views>`__
- `Use "~all" to Expand All Available
Fields <#use-all-to-expand-all-available-fields>`__
- `Dynamically Setting Fields/Sparse
Fieldsets <#dynamically-setting-fields>`__
- `From URL Parameters <#from-url-parameters>`__
- `From Serializer Options <#from-serializer-options>`__
- `Combining Dynamically-Set Fields and Field
Expansion <#combining-dynamically-set-fields-and-field-expansion>`__
- `Serializer Introspection <#serializer-introspection>`__
- `Lazy evaluation of serializer <#lazy-evaluation-of-serializer>`__
- `Query optimization
(experimental) <#query-optimization-experimental>`__
- `Change Log <#changelog>`__
- `Testing <#testing>`__
- `License <#license>`__
Installation
============
::
pip install drf-flex-fields
Requirements
============
- Python >= 2.7
- Django >= 1.8
Basics
======
To use this package's functionality, your serializers need to subclass
``FlexFieldsModelSerializer`` or use the provided
``FlexFieldsSerializerMixin``. If you would like built-in protection for
controlling when clients are allowed to expand resources when listing
resource collections, your viewsets need to subclass
``FlexFieldsModelViewSet``.
.. code:: python
from rest_flex_fields import FlexFieldsModelViewSet, FlexFieldsModelSerializer
class PersonViewSet(FlexFieldsModelViewSet):
queryset = models.Person.objects.all()
serializer_class = PersonSerializer
# Whitelist fields that can be expanded when listing resources
permit_list_expands = ['country']
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name', 'population')
class PersonSerializer(FlexFieldsModelSerializer):
class Meta:
model = Person
fields = ('id', 'name', 'country', 'occupation')
expandable_fields = {
'country': (CountrySerializer, {'source': 'country'})
}
Now you can make requests like
``GET /person?expand=country&fields=id,name,country`` to dynamically
manipulate which fields are included, as well as expand primitive fields
into nested objects. You can also use dot notation to control both the
``fields`` and ``expand`` settings at arbitrary levels of depth in your
serialized responses. Read on to learn the details and see more complex
examples.
:heavy\_check\_mark: The examples below subclass
``FlexFieldsModelSerializer``, but the same can be accomplished by
mixing in ``FlexFieldsSerializerMixin``, which is also importable from
the same ``rest_flex_fields`` package.
Dynamic Field Expansion
=======================
To define an expandable field, add it to the ``expandable_fields``
within your serializer:
.. code:: python
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': (CountrySerializer, {'source': 'country', 'fields': ['name']})
}
If the default serialized response is the following:
.. code:: json
{
"id" : 13322,
"name" : "John Doe",
"country" : 12,
"occupation" : "Programmer",
}
When you do a ``GET /person/13322?expand=country``, the response will
change to:
.. code:: json
{
"id" : 13322,
"name" : "John Doe",
"country" : {
"name" : "United States"
},
"occupation" : "Programmer",
}
Notice how ``population`` was ommitted from the nested ``country``
object. This is because ``fields`` was set to ``['name']`` when passed
to the embedded ``CountrySerializer``. You will learn more about this
later on.
Deferred Fields
---------------
Alternatively, you could treat ``country`` as a "deferred" field by not
defining it among the default fields. To make a field deferred, only
define it within the serializer's ``expandable_fields``.
Deep, Nested Expansion
----------------------
Let's say you add ``StateSerializer`` as serializer nested inside the
country serializer above:
.. code:: python
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ['name', 'population']
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
expandable_fields = {
'states': (StateSerializer, {'source': 'states', 'many': True})
}
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': (CountrySerializer, {'source': 'country', 'fields': ['name']})
}
Your default serialized response might be the following for ``person``
and ``country``, respectively:
.. code:: json
{
"id" : 13322,
"name" : "John Doe",
"country" : 12,
"occupation" : "Programmer",
}
{
"id" : 12,
"name" : "United States",
"states" : "http://www.api.com/countries/12/states"
}
But if you do a ``GET /person/13322?expand=country.states``, it would
be:
.. code:: json
{
"id" : 13322,
"name" : "John Doe",
"occupation" : "Programmer",
"country" : {
"id" : 12,
"name" : "United States",
"states" : [
{
"name" : "Ohio",
"population": 11000000
}
]
}
}
Please be kind to your database, as this could incur many additional
queries. Though, you can mitigate this impact through judicious use of
``prefetch_related`` and ``select_related`` when defining the queryset
for your viewset.
Configuration from Serializer Options
-------------------------------------
You could accomplish the same result (expanding the ``states`` field
within the embedded country serializer) by explicitly passing the
``expand`` option within your serializer:
.. code:: python
class PersonSerializer(FlexFieldsModelSerializer):
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': (CountrySerializer, {'source': 'country', 'expand': ['states']})
}
Field Expansion on "List" Views
-------------------------------
By default, when subclassing ``FlexFieldsModelViewSet``, you can only
expand fields when you are retrieving single resources, in order to
protect yourself from careless clients. However, if you would like to
make a field expandable even when listing collections, you can add the
field's name to the ``permit_list_expands`` property on the viewset.
Just make sure you are wisely using ``select_related`` and
``prefetch_related`` in the viewset's queryset. You can take advantage
of a utility function, ``is_expanded()`` to adjust the queryset
accordingly.
Example:
.. code:: python
from drf_flex_fields import is_expanded
class PersonViewSet(FlexFieldsModelViewSet):
permit_list_expands = ['employer']
serializer_class = PersonSerializer
def get_queryset(self):
queryset = models.Person.objects.all()
if is_expanded(self.request, 'employer'):
queryset = queryset.select_related('employer')
return queryset
Use "~all" to Expand All Available Fields
-----------------------------------------
You can set ``expand=~all`` to automatically expand all fields that are
available for expansion. This will take effect only for the top-level
serializer; if you need to also expand fields that are present on deeply
nested models, then you will need to explicitly pass their values using
dot notation.
Dynamically Setting Fields (Sparse Fields)
==========================================
You can use either they ``fields`` or ``omit`` keywords to declare only
the fields you want to include or to specify fields that should be
excluded.
From URL Parameters
-------------------
You can dynamically set fields, with the configuration originating from
the URL parameters or serializer options.
Consider this as a default serialized response:
.. code:: json
{
"id" : 13322,
"name" : "John Doe",
"country" : {
"name" : "United States",
"population": 330000000
},
"occupation" : "Programmer",
"hobbies" : ["rock climbing", "sipping coffee"]
}
To whittle down the fields via URL parameters, simply add
``?fields=id,name,country`` to your requests to get back:
.. code:: json
{
"id" : 13322,
"name" : "John Doe",
"country" : {
"name" : "United States",
"population: 330000000
}
}
Or, for more specificity, you can use dot-notation,
``?fields=id,name,country.name``:
.. code:: json
{
"id" : 13322,
"name" : "John Doe",
"country" : {
"name" : "United States",
}
}
Or, if you want to leave out the nested country object, do
``?omit=country``:
.. code:: json
{
"id" : 13322,
"name" : "John Doe",
"occupation" : "Programmer",
"hobbies" : ["rock climbing", "sipping coffee"]
}
From Serializer Options
-----------------------
You could accomplish the same outcome as the example above by passing
options to your serializers. With this approach, you lose runtime
dynamism, but gain the ability to re-use serializers, rather than
creating a simplified copy of a serializer for the purposes of embedding
it. The example below uses the ``fields`` keyword, but you can also pass
in keyword argument for ``omit`` to exclude specific fields.
.. code:: python
from rest_flex_fields import FlexFieldsModelSerializer
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['id', 'name', 'population']
class PersonSerializer(FlexFieldsModelSerializer):
country: CountrySerializer(fields=['name'])
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation', 'hobbies']
serializer = PersonSerializer(person, fields=["id", "name", "country.name"])
print(serializer.data)
>>>{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
}
}
Combining Dynamically Set Fields and Field Expansion
====================================================
You may be wondering how things work if you use both the ``expand`` and
``fields`` option, and there is overlap. For example, your serialized
person model may look like the following by default:
.. code:: json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
}
}
However, you make the following request
``HTTP GET /person/13322?include=id,name&expand=country``. You will get
the following back:
.. code:: json
{
"id": 13322,
"name": "John Doe"
}
The ``include`` field takes precedence over ``expand``. That is, if a
field is not among the set that is explicitly alllowed, it cannot be
expanded. If such a conflict occurs, you will not pay for the extra
database queries - the expanded field will be silently abandoned.
Serializer Introspection
========================
When using an instance of ``FlexFieldsModelSerializer``, you can examine
the property ``expanded_fields`` to discover which fields, if any, have
been dynamically expanded.
Lazy evaluation of serializer
=============================
If you want to lazily evaluate the reference to your nested serializer
class from a string inside expandable\_fields, you need to use this
syntax:
.. code:: python
expandable_fields = {
'record_set': ('.RelatedSerializer', {'source': 'related_set', 'many': True})
}
Substitute the name of your Django app where the serializer is found for
````.
This allows to reference a serializer that has not yet been defined.
Query optimization (experimental)
=================================
An experimental filter backend is available to help you automatically
reduce the number of SQL queries and their transfer size. *This feature
has not been tested thorougly and any help testing and reporting bugs is
greatly appreciated.* You can add FlexFieldFilterBackend to
``DEFAULT_FILTER_BACKENDS`` in the settings:
.. code:: python
# settings.py
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'rest_flex_fields.filter_backends.FlexFieldsFilterBackend',
# ...
),
# ...
}
It will automatically call ``select_related`` and ``prefetch_related``
on the current QuerySet by determining which fields are needed from
many-to-many and foreign key-related models. For sparse fields requests
(``?omit=fieldX,fieldY`` or ``?fields=fieldX,fieldY``), the backend will
automatically call ``only(*field_names)`` using only the fields needed
for serialization.
**WARNING:** The optimization currently works only for one nesting
level.
Changelog
==========
0.6.0 (May 2019)
----------------
- Adds experimental support for automatically SQL query optimization
via a ``FlexFieldsFilterBackend``. Thanks ADR-007!
- Adds CircleCI config file. Thanks mikeIFTS!
- Moves declaration of ``expandable_fields`` to ``Meta`` class on
serialzer for consistency with DRF (will continue to support
declaration as class property)
0.5.0 (April 2019)
------------------
- Added support for ``omit`` keyword for field exclusion. Code clean up
and improved test coverage.
0.3.4 (May 2018)
----------------
- Handle case where ``request`` is ``None`` when accessing request
object from serializer. Thanks @jsatt!
0.3.3 (April 2018)
------------------
- Exposes ``FlexFieldsSerializerMixin`` in addition to
``FlexFieldsModelSerializer``. Thanks @jsatt!
Testing
=======
Tests are found in a simplified DRF project in the ``/tests`` folder.
Install the project requirements and do ``./manage.py test`` to run
them.
License
=======
See `License `__.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1678541408.2903485
drf-flex-fields-1.0.2/drf_flex_fields.egg-info/ 0000775 0001750 0001750 00000000000 14403101140 020700 5 ustar 00robert robert ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678541408.0
drf-flex-fields-1.0.2/drf_flex_fields.egg-info/PKG-INFO 0000664 0001750 0001750 00000065336 14403101140 022012 0 ustar 00robert robert Metadata-Version: 2.1
Name: drf-flex-fields
Version: 1.0.2
Summary: Flexible, dynamic fields and nested resources for Django REST Framework serializers.
Home-page: https://github.com/rsinger86/drf-flex-fields
Author: Robert Singer
Author-email: robertgsinger@gmail.com
License: MIT
Keywords: django rest api dynamic fields
Platform: UNKNOWN
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Description-Content-Type: text/markdown
License-File: LICENSE
# Django REST - FlexFields
[](https://pypi.python.org/pypi/drf-flex-fields)
[](https://img.shields.io/pypi/status/django-lifecycle.svg/)
Flexible, dynamic fields and nested models for Django REST Framework serializers.
# Overview
FlexFields (DRF-FF) for [Django REST Framework](https://django-rest-framework.org) is a package designed to provide a common baseline of functionality for dynamically setting fields and nested models within DRF serializers. This package is designed for simplicity, with minimal magic and entanglement with DRF's foundational classes.
Key benefits:
- Easily set up fields that be expanded to their fully serialized counterparts via query parameters (`users/?expand=organization,friends`)
- Select a subset of fields by either:
- specifying which ones should be included (`users/?fields=id,first_name`)
- specifying which ones should be excluded (`users/?omit=id,first_name`)
- Use dot notation to dynamically modify fields at arbitrary depths (`users/?expand=organization.owner.roles`)
- Flexible API - options can also be passed directly to a serializer: `UserSerializer(obj, expand=['organization'])`
# Quick Start
```python
from rest_flex_fields import FlexFieldsModelSerializer
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ('id', 'name')
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name', 'population', 'states')
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
class PersonSerializer(FlexFieldsModelSerializer):
class Meta:
model = Person
fields = ('id', 'name', 'country', 'occupation')
expandable_fields = {'country': CountrySerializer}
```
```
GET /people/142/
```
```json
{
"id": 142,
"name": "Jim Halpert",
"country": 1
}
```
```
GET /people/142/?expand=country.states
```
```json
{
"id": 142,
"name": "Jim Halpert",
"country": {
"id": 1,
"name": "United States",
"states": [
{
"id": 23,
"name": "Ohio"
},
{
"id": 2,
"name": "Pennsylvania"
}
]
}
}
```
# Table of Contents:
- [Django REST - FlexFields](#django-rest---flexfields)
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Table of Contents:](#table-of-contents)
- [Setup](#setup)
- [Usage](#usage)
- [Dynamic Field Expansion](#dynamic-field-expansion)
- [Deferred Fields](#deferred-fields)
- [Deep, Nested Expansion](#deep-nested-expansion)
- [Field Expansion on "List" Views ](#field-expansion-on-list-views-)
- [Expanding a "Many" Relationship ](#expanding-a-many-relationship-)
- [Dynamically Setting Fields (Sparse Fields) ](#dynamically-setting-fields-sparse-fields-)
- [Reference serializer as a string (lazy evaluation) ](#reference-serializer-as-a-string-lazy-evaluation-)
- [Increased re-usability of serializers ](#increased-re-usability-of-serializers-)
- [Serializer Options](#serializer-options)
- [Advanced](#advanced)
- [Customization](#customization)
- [Serializer Introspection](#serializer-introspection)
- [Use Wildcards to Match Multiple Fields](#wildcards)
- [Combining Sparse Fields and Field Expansion ](#combining-sparse-fields-and-field-expansion-)
- [Utility Functions ](#utility-functions-)
- [rest_flex_fields.is_expanded(request, field: str)](#rest_flex_fieldsis_expandedrequest-field-str)
- [rest_flex_fields.is_included(request, field: str)](#rest_flex_fieldsis_includedrequest-field-str)
- [Query optimization (experimental)](#query-optimization-experimental)
- [Changelog ](#changelog-)
- [Testing](#testing)
- [License](#license)
# Setup
First install:
```
pip install drf-flex-fields
```
Then have your serializers subclass `FlexFieldsModelSerializer`:
```python
from rest_flex_fields import FlexFieldsModelSerializer
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name')
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ('id', 'name', 'population', 'states')
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
```
Alternatively, you can add the `FlexFieldsSerializerMixin` mixin to a model serializer.
# Usage
## Dynamic Field Expansion
To define expandable fields, add an `expandable_fields` dictionary to your serializer's `Meta` class. Key the dictionary with the name of the field that you want to dynamically expand, and set its value to either the expanded serializer or a tuple where the first element is the serializer and the second is a dictionary of options that will be used to instantiate the serializer.
```python
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': CountrySerializer
}
```
If the default serialized response is the following:
```json
{
"id": 13322,
"name": "John Doe",
"country": 12,
"occupation": "Programmer"
}
```
When you do a `GET /person/13322?expand=country`, the response will change to:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
"population": 330000000
},
"occupation": "Programmer"
}
```
## Deferred Fields
Alternatively, you could treat `country` as a "deferred" field by not defining it among the default fields. To make a field deferred, only define it within the serializer's `expandable_fields`.
## Deep, Nested Expansion
Let's say you add `StateSerializer` as a serializer nested inside the country serializer above:
```python
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ['name', 'population']
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
class PersonSerializer(FlexFieldsModelSerializer):
country = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Person
fields = ['id', 'name', 'country', 'occupation']
expandable_fields = {
'country': CountrySerializer
}
```
Your default serialized response might be the following for `person` and `country`, respectively:
```json
{
"id" : 13322,
"name" : "John Doe",
"country" : 12,
"occupation" : "Programmer",
}
{
"id" : 12,
"name" : "United States",
"states" : "http://www.api.com/countries/12/states"
}
```
But if you do a `GET /person/13322?expand=country.states`, it would be:
```json
{
"id": 13322,
"name": "John Doe",
"occupation": "Programmer",
"country": {
"id": 12,
"name": "United States",
"states": [
{
"name": "Ohio",
"population": 11000000
}
]
}
}
```
Please be kind to your database, as this could incur many additional queries. Though, you can mitigate this impact through judicious use of `prefetch_related` and `select_related` when defining the queryset for your viewset.
## Field Expansion on "List" Views
If you request many objects, expanding fields could lead to many additional database queries. Subclass `FlexFieldsModelViewSet` if you want to prevent expanding fields by default when calling a ViewSet's `list` method. Place those fields that you would like to expand in a `permit_list_expands` property on the ViewSet:
```python
from rest_flex_fields import is_expanded
class PersonViewSet(FlexFieldsModelViewSet):
permit_list_expands = ['employer']
serializer_class = PersonSerializer
def get_queryset(self):
queryset = models.Person.objects.all()
if is_expanded(self.request, 'employer'):
queryset = queryset.select_related('employer')
return queryset
```
Notice how this example is using the `is_expanded` utility method as well as `select_related` and `prefetch_related` to efficiently query the database if the field is expanded.
## Expanding a "Many" Relationship
Set `many` to `True` in the serializer options to make sure "to many" fields are expanded correctly.
```python
class StateSerializer(FlexFieldsModelSerializer):
class Meta:
model = State
fields = ['name', 'population']
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['name', 'population']
expandable_fields = {
'states': (StateSerializer, {'many': True})
}
```
A request to `GET /countries?expand=states` will return:
```python
{
"id" : 12,
"name" : "United States",
"states" : [
{
"name" : "Alabama",
"population": 11000000
},
//... more states ... //
{
"name" : "Ohio",
"population": 11000000
}
]
}
```
## Dynamically Setting Fields (Sparse Fields)
You can use either the `fields` or `omit` keywords to declare only the fields you want to include or to specify fields that should be excluded.
Consider this as a default serialized response:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
"population": 330000000
},
"occupation": "Programmer",
"hobbies": ["rock climbing", "sipping coffee"]
}
```
To whittle down the fields via URL parameters, simply add `?fields=id,name,country` to your requests to get back:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States",
"population": 330000000
}
}
```
Or, for more specificity, you can use dot-notation, `?fields=id,name,country.name`:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States"
}
}
```
Or, if you want to leave out the nested country object, do `?omit=country`:
```json
{
"id": 13322,
"name": "John Doe",
"occupation": "Programmer",
"hobbies": ["rock climbing", "sipping coffee"]
}
```
## Reference serializer as a string (lazy evaluation)
To avoid circular import problems, it's possible to lazily evaluate a string reference to you serializer class using this syntax:
```python
expandable_fields = {
'record_set': ('.RelatedSerializer', {'many': True})
}
```
**Note**:
Prior to version `0.9.0`, it was assumed your serializer classes would be in a module with the following path:
`.serializers`.
This import style will still work, but you can also now specify fully-qualified import paths to any locations.
## Increased re-usability of serializers
The `omit` and `fields` options can be passed directly to serializers. Rather than defining a separate, slimmer version of a regular serializer, you can re-use the same serializer and declare which fields you want.
```python
from rest_flex_fields import FlexFieldsModelSerializer
class CountrySerializer(FlexFieldsModelSerializer):
class Meta:
model = Country
fields = ['id', 'name', 'population', 'capital', 'square_miles']
class PersonSerializer(FlexFieldsModelSerializer):
country = CountrySerializer(fields=['id', 'name'])
class Meta:
model = Person
fields = ['id', 'name', 'country']
serializer = PersonSerializer(person)
print(serializer.data)
>>>{
"id": 13322,
"name": "John Doe",
"country": {
"id": 1,
"name": "United States",
}
}
```
# Serializer Options
Dynamic field options can be passed in the following ways:
- from the request's query parameters; separate multiple values with a commma
- as keyword arguments directly to the serializer class when its constructed
- from a dictionary placed as the second element in a tuple when defining `expandable_fields`
Approach #1
```
GET /people?expand=friends.hobbies,employer&omit=age
```
Approach #2
```python
serializer = PersonSerializer(
person,
expand=["friends.hobbies", "employer"],
omit="friends.age"
)
```
Approach #3
```python
class PersonSerializer(FlexFieldsModelSerializer):
// Your field definitions
class Meta:
model = Person
fields = ["age", "hobbies", "name"]
expandable_fields = {
'friends': (
'serializer.FriendSerializer',
{'many': True, "expand": ["hobbies"], "omit": ["age"]}
)
}
```
| Option | Description |
| ------ | :--------------------------------------------------------------------------: |
| expand | Fields to expand; must be configured in the serializer's `expandable_fields` |
| fields | Fields that should be included; all others will be excluded |
| omit | Fields that should be excluded; all others will be included |
# Advanced
## Customization
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` |
| 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` |
| WILDCARD_VALUES | List of values that stand in for all field names. Can be used with the `fields` and `expand` parameters.
When used with `expand`, a wildcard value will trigger the expansion of all `expandable_fields` at a given level.
When used with `fields`, all fields are included at a given level. For example, you could pass `fields=name,state.*` if you have a city resource with a nested state in order to expand only the city's name field and all of the state's fields.
To disable use of wildcards, set this setting to `None`. | `["*", "~all"]` |
For example, if you want your API to work a bit more like [JSON API](https://jsonapi.org/format/#fetching-includes), you could do:
```python
REST_FLEX_FIELDS = {"EXPAND_PARAM": "include"}
```
### Defining Expansion and Recursive Limits on Serializer Classes
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.
## Serializer Introspection
When using an instance of `FlexFieldsModelSerializer`, you can examine the property `expanded_fields` to discover which fields, if any, have been dynamically expanded.
## Use of Wildcard to Match All Fields
You can pass `expand=*` ([or another value of your choosing](#customization)) to automatically expand all fields that are available for expansion at a given level. To refer to nested resources, you can use dot-notation. For example, requesting `expand=menu.sections` for a restaurant resource would expand its nested `menu` resource, as well as that menu's nested `sections` resource.
Or, when requesting sparse fields, you can pass `fields=*` to include only the specified fields at a given level. To refer to nested resources, you can use dot-notation. For example, if you have an `order` resource, you could request all of its fields as well as only two fields on its nested `restaurant` resource with the following: `fields=*,restaurent.name,restaurant.address&expand=restaurant`.
## Combining Sparse Fields and Field Expansion
You may be wondering how things work if you use both the `expand` and `fields` option, and there is overlap. For example, your serialized person model may look like the following by default:
```json
{
"id": 13322,
"name": "John Doe",
"country": {
"name": "United States"
}
}
```
However, you make the following request `HTTP GET /person/13322?include=id,name&expand=country`. You will get the following back:
```json
{
"id": 13322,
"name": "John Doe"
}
```
The `fields` parameter takes precedence over `expand`. That is, if a field is not among the set that is explicitly alllowed, it cannot be expanded. If such a conflict occurs, you will not pay for the extra database queries - the expanded field will be silently abandoned.
## Utility Functions
### rest_flex_fields.is_expanded(request, field: str)
Checks whether a field has been expanded via the request's query parameters.
**Parameters**
- **request**: The request object
- **field**: The name of the field to check
### rest_flex_fields.is_included(request, field: str)
Checks whether a field has NOT been excluded via either the `omit` parameter or the `fields` parameter.
**Parameters**
- **request**: The request object
- **field**: The name of the field to check
## Query optimization (experimental)
An experimental filter backend is available to help you automatically reduce the number of SQL queries and their transfer size. _This feature has not been tested thorougly and any help testing and reporting bugs is greatly appreciated._ You can add FlexFieldFilterBackend to `DEFAULT_FILTER_BACKENDS` in the settings:
```python
# settings.py
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'rest_flex_fields.filter_backends.FlexFieldsFilterBackend',
# ...
),
# ...
}
```
It will automatically call `select_related` and `prefetch_related` on the current QuerySet by determining which fields are needed from many-to-many and foreign key-related models. For sparse fields requests (`?omit=fieldX,fieldY` or `?fields=fieldX,fieldY`), the backend will automatically call `only(*field_names)` using only the fields needed for serialization.
**WARNING:** The optimization currently works only for one nesting level.
# Changelog
## 1.0.2 (March 2023)
- Adds control over whether recursive expansions are allowed and allows setting the max expansion depth. Thanks @andruten!
## 1.0.1 (March 2023)
- Various bug fixes. Thanks @michaelschem, @andruten, and @erielias!
## 1.0.0 (August 2022)
- Improvements to the filter backends for generic foreign key handling and docs generation. Thanks @KrYpTeD974 and @michaelschem!
## 0.9.9 (July 2022)
- Fixes bug in `FlexFieldsFilterBackend`. Thanks @michaelschem!
- Adds `FlexFieldsDocsFilterBackend` for schema population. Thanks @Rjevski!
## 0.9.8 (April 2022)
- Set expandable fields as the default example for expand query parameters in `coreapi.Field`. Thanks @JasperSui!
## 0.9.7 (January 2022)
- Includes m2m in prefetch_related clause even if they're not expanded. Thanks @pablolmedorado and @ADR-007!
## 0.9.6 (November 2021)
- Make it possible to use wildcard values with sparse fields requests.
## 0.9.5 (October 2021)
- Adds OpenAPI support. Thanks @soroush-tabesh!
- Updates tests for Django 3.2 and fixes deprecation warning. Thanks @giovannicimolin!
## 0.9.3 (August 2021)
- Fixes bug where custom parameter names were not passed when constructing nested serializers. Thanks @Kandeel4411!
## 0.9.2 (June 2021)
- Ensures `context` dict is passed down to expanded serializers. Thanks @nikeshyad!
## 0.9.1 (June 2021)
- No longer auto removes `source` argument if it's equal to the field name.
## 0.9.0 (April 2021)
- Allows fully qualified import strings for lazy serializer classes.
## 0.8.9 (February 2021)
- Adds OpenAPI support to experimental filter backend. Thanks @LukasBerka!
## 0.8.8 (September 2020)
- Django 3.1.1 fix. Thansks @NiyazNz!
- Docs typo fix. Thanks @zakjholt!
## 0.8.6 (September 2020)
- Adds `is_included` utility function.
## 0.8.5 (May 2020)
- Adds options to customize parameter names and wildcard values. Closes #10.
## 0.8.1 (May 2020)
- Fixes #44, related to the experimental filter backend. Thanks @jsatt!
## 0.8.0 (April 2020)
- Adds support for `expand`, `omit` and `fields` query parameters for non-GET requests.
- The common use case is creating/updating a model instance and returning a serialized response with expanded fields
- Thanks @kotepillar for raising the issue (#25) and @Crocmagnon for the idea of delaying field modification to `to_representation()`.
## 0.7.5 (February 2020)
- Simplifies declaration of `expandable_fields`
- If using a tuple, the second element - to define the serializer settings - is now optional.
- Instead of a tuple, you can now just use the serializer class or a string to lazily reference that class.
- Updates documentation.
## 0.7.0 (February 2020)
- Adds support for different ways of passing arrays in query strings. Thanks @sentyaev!
- Fixes attribute error when map is supplied to split levels utility function. Thanks @hemache!
## 0.6.1 (September 2019)
- Adds experimental support for automatically SQL query optimization via a `FlexFieldsFilterBackend`. Thanks ADR-007!
- Adds CircleCI config file. Thanks mikeIFTS!
- Moves declaration of `expandable_fields` to `Meta` class on serialzer for consistency with DRF (will continue to support declaration as class property)
- Python 2 is no longer supported. If you need Python 2 support, you can continue to use older versions of this package.
## 0.5.0 (April 2019)
- Added support for `omit` keyword for field exclusion. Code clean up and improved test coverage.
## 0.3.4 (May 2018)
- Handle case where `request` is `None` when accessing request object from serializer. Thanks @jsatt!
## 0.3.3 (April 2018)
- Exposes `FlexFieldsSerializerMixin` in addition to `FlexFieldsModelSerializer`. Thanks @jsatt!
# Testing
Tests are found in a simplified DRF project in the `/tests` folder. Install the project requirements and do `./manage.py test` to run them.
# License
See [License](LICENSE.md).
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678541408.0
drf-flex-fields-1.0.2/drf_flex_fields.egg-info/SOURCES.txt 0000664 0001750 0001750 00000001174 14403101140 022567 0 ustar 00robert robert LICENSE
MANIFEST.in
README.md
README.txt
setup.py
drf_flex_fields.egg-info/PKG-INFO
drf_flex_fields.egg-info/SOURCES.txt
drf_flex_fields.egg-info/dependency_links.txt
drf_flex_fields.egg-info/top_level.txt
rest_flex_fields/__init__.py
rest_flex_fields/filter_backends.py
rest_flex_fields/serializers.py
rest_flex_fields/utils.py
rest_flex_fields/views.py
tests/__init__.py
tests/settings.py
tests/test_flex_fields_model_serializer.py
tests/test_serializer.py
tests/test_utils.py
tests/test_views.py
tests/urls.py
tests/testapp/__init__.py
tests/testapp/apps.py
tests/testapp/models.py
tests/testapp/serializers.py
tests/testapp/views.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678541408.0
drf-flex-fields-1.0.2/drf_flex_fields.egg-info/dependency_links.txt 0000664 0001750 0001750 00000000001 14403101140 024746 0 ustar 00robert robert
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678541408.0
drf-flex-fields-1.0.2/drf_flex_fields.egg-info/top_level.txt 0000664 0001750 0001750 00000000021 14403101140 023423 0 ustar 00robert robert rest_flex_fields
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1678541408.2903485
drf-flex-fields-1.0.2/rest_flex_fields/ 0000775 0001750 0001750 00000000000 14403101140 017410 5 ustar 00robert robert ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678540543.0
drf-flex-fields-1.0.2/rest_flex_fields/__init__.py 0000664 0001750 0001750 00000003004 14403077377 021546 0 ustar 00robert robert from django.conf import settings
FLEX_FIELDS_OPTIONS = getattr(settings, "REST_FLEX_FIELDS", {})
EXPAND_PARAM = FLEX_FIELDS_OPTIONS.get("EXPAND_PARAM", "expand")
FIELDS_PARAM = FLEX_FIELDS_OPTIONS.get("FIELDS_PARAM", "fields")
OMIT_PARAM = FLEX_FIELDS_OPTIONS.get("OMIT_PARAM", "omit")
MAXIMUM_EXPANSION_DEPTH = FLEX_FIELDS_OPTIONS.get("MAXIMUM_EXPANSION_DEPTH", None)
RECURSIVE_EXPANSION_PERMITTED = FLEX_FIELDS_OPTIONS.get(
"RECURSIVE_EXPANSION_PERMITTED", True
)
WILDCARD_ALL = "~all"
WILDCARD_ASTERISK = "*"
if "WILDCARD_EXPAND_VALUES" in FLEX_FIELDS_OPTIONS:
WILDCARD_VALUES = FLEX_FIELDS_OPTIONS["WILDCARD_EXPAND_VALUES"]
elif "WILDCARD_VALUES" in FLEX_FIELDS_OPTIONS:
WILDCARD_VALUES = FLEX_FIELDS_OPTIONS["WILDCARD_VALUES"]
else:
WILDCARD_VALUES = [WILDCARD_ALL, WILDCARD_ASTERISK]
assert isinstance(EXPAND_PARAM, str), "'EXPAND_PARAM' should be a string"
assert isinstance(FIELDS_PARAM, str), "'FIELDS_PARAM' should be a string"
assert isinstance(OMIT_PARAM, str), "'OMIT_PARAM' should be a string"
if type(WILDCARD_VALUES) not in (list, type(None)):
raise ValueError("'WILDCARD_EXPAND_VALUES' should be a list of strings or None")
if type(MAXIMUM_EXPANSION_DEPTH) not in (int, type(None)):
raise ValueError("'MAXIMUM_EXPANSION_DEPTH' should be a int or None")
if type(RECURSIVE_EXPANSION_PERMITTED) is not bool:
raise ValueError("'RECURSIVE_EXPANSION_PERMITTED' should be a bool")
from .utils import *
from .serializers import FlexFieldsModelSerializer
from .views import FlexFieldsModelViewSet
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/rest_flex_fields/filter_backends.py 0000664 0001750 0001750 00000021311 14400315017 023106 0 ustar 00robert robert from functools import lru_cache
from typing import Optional
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from django.db.models import QuerySet
from rest_framework.compat import coreapi, coreschema
from rest_framework.filters import BaseFilterBackend
from rest_framework.request import Request
from rest_framework.viewsets import GenericViewSet
from rest_flex_fields import (
FIELDS_PARAM,
EXPAND_PARAM,
OMIT_PARAM,
WILDCARD_VALUES
)
WILDCARD_VALUES_JOINED = ",".join(WILDCARD_VALUES)
from rest_flex_fields.serializers import (
FlexFieldsModelSerializer,
FlexFieldsSerializerMixin,
)
class FlexFieldsDocsFilterBackend(BaseFilterBackend):
"""
A dummy filter backend only for schema/documentation purposes.
"""
def filter_queryset(self, request, queryset, view):
return queryset
@staticmethod
@lru_cache()
def _get_field(field_name: str, model: models.Model) -> Optional[models.Field]:
try:
# noinspection PyProtectedMember
return model._meta.get_field(field_name)
except FieldDoesNotExist:
return None
@staticmethod
def _get_expandable_fields(serializer_class: FlexFieldsModelSerializer) -> list:
expandable_fields = list(getattr(serializer_class.Meta, 'expandable_fields').items())
expand_list = []
while expandable_fields:
key, cls = expandable_fields.pop()
cls = cls[0] if hasattr(cls, '__iter__') else cls
expand_list.append(key)
if hasattr(cls, "Meta") and issubclass(cls, FlexFieldsSerializerMixin) and hasattr(cls.Meta, "expandable_fields"):
next_layer = getattr(cls.Meta, 'expandable_fields')
expandable_fields.extend([(f"{key}.{k}", cls) for k, cls in list(next_layer.items())])
return expand_list
@staticmethod
def _get_fields(serializer_class):
fields = getattr(serializer_class.Meta, "fields", [])
return ",".join(fields)
def get_schema_fields(self, view):
assert (
coreapi is not None
), "coreapi must be installed to use `get_schema_fields()`"
assert (
coreschema is not None
), "coreschema must be installed to use `get_schema_fields()`"
serializer_class = view.get_serializer_class()
if not issubclass(serializer_class, FlexFieldsSerializerMixin):
return []
fields = self._get_fields(serializer_class)
expandable_fields_joined = ",".join(self._get_expandable_fields(serializer_class))
return [
coreapi.Field(
name=FIELDS_PARAM,
required=False,
location="query",
schema=coreschema.String(
title="Selected fields",
description="Specify required fields by comma",
),
example=(fields or "field1,field2,nested.field") + "," + WILDCARD_VALUES_JOINED,
),
coreapi.Field(
name=OMIT_PARAM,
required=False,
location="query",
schema=coreschema.String(
title="Omitted fields",
description="Specify omitted fields by comma",
),
example=(fields or "field1,field2,nested.field") + "," + WILDCARD_VALUES_JOINED,
),
coreapi.Field(
name=EXPAND_PARAM,
required=False,
location="query",
schema=coreschema.String(
title="Expanded fields",
description="Specify expanded fields by comma",
),
example=(expandable_fields_joined or "field1,field2,nested.field") + "," + WILDCARD_VALUES_JOINED,
),
]
def get_schema_operation_parameters(self, view):
serializer_class = view.get_serializer_class()
if not issubclass(serializer_class, FlexFieldsSerializerMixin):
return []
fields = self._get_fields(serializer_class)
expandable_fields = self._get_expandable_fields(serializer_class)
expandable_fields.extend(WILDCARD_VALUES)
parameters = [
{
"name": FIELDS_PARAM,
"required": False,
"in": "query",
"description": "Specify required fields by comma",
"schema": {
"title": "Selected fields",
"type": "string",
},
"example": (fields or "field1,field2,nested.field") + "," + WILDCARD_VALUES_JOINED,
},
{
"name": OMIT_PARAM,
"required": False,
"in": "query",
"description": "Specify omitted fields by comma",
"schema": {
"title": "Omitted fields",
"type": "string",
},
"example": (fields or "field1,field2,nested.field") + "," + WILDCARD_VALUES_JOINED,
},
{
"name": EXPAND_PARAM,
"required": False,
"in": "query",
"description": "Select fields to expand",
"style": "form",
"explode": False,
"schema": {
"title": "Expanded fields",
"type": "array",
"items": {
"type": "string",
"enum": expandable_fields
}
},
},
]
return parameters
class FlexFieldsFilterBackend(FlexFieldsDocsFilterBackend):
def filter_queryset(
self, request: Request, queryset: QuerySet, view: GenericViewSet
):
if (
not issubclass(view.get_serializer_class(), FlexFieldsSerializerMixin)
or request.method != "GET"
):
return queryset
auto_remove_fields_from_query = getattr(
view, "auto_remove_fields_from_query", True
)
auto_select_related_on_query = getattr(
view, "auto_select_related_on_query", True
)
required_query_fields = list(getattr(view, "required_query_fields", []))
serializer = view.get_serializer( # type: FlexFieldsSerializerMixin
context=view.get_serializer_context()
)
serializer.apply_flex_fields(
serializer.fields, serializer._flex_options_rep_only
)
serializer._flex_fields_rep_applied = True
model_fields = []
nested_model_fields = []
for field in serializer.fields.values():
model_field = self._get_field(field.source, queryset.model)
if model_field:
model_fields.append(model_field)
if field.field_name in serializer.expanded_fields or \
(model_field.is_relation and not model_field.many_to_one) or \
(model_field.is_relation and model_field.many_to_one and not model_field.concrete): # Include GenericForeignKey
nested_model_fields.append(model_field)
if auto_remove_fields_from_query:
queryset = queryset.only(
*(
required_query_fields
+ [
model_field.name
for model_field in model_fields if (
not model_field.is_relation or
model_field.many_to_one and model_field.concrete)
]
)
)
if auto_select_related_on_query and nested_model_fields:
queryset = queryset.select_related(
*(
model_field.name
for model_field in nested_model_fields if (
model_field.is_relation and
model_field.many_to_one and
model_field.concrete) # Exclude GenericForeignKey
)
)
queryset = queryset.prefetch_related(*(
model_field.name for model_field in nested_model_fields if
(model_field.is_relation and not model_field.many_to_one) or
(model_field.is_relation and model_field.many_to_one and not model_field.concrete) # Include GenericForeignKey)
)
)
return queryset
@staticmethod
@lru_cache()
def _get_field(field_name: str, model: models.Model) -> Optional[models.Field]:
try:
# noinspection PyProtectedMember
return model._meta.get_field(field_name)
except FieldDoesNotExist:
return None
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678540855.0
drf-flex-fields-1.0.2/rest_flex_fields/serializers.py 0000664 0001750 0001750 00000030773 14403100067 022337 0 ustar 00robert robert import copy
import importlib
from typing import List, Optional, Tuple
from rest_framework import serializers
from rest_flex_fields import (
EXPAND_PARAM,
FIELDS_PARAM,
OMIT_PARAM,
WILDCARD_VALUES,
MAXIMUM_EXPANSION_DEPTH,
RECURSIVE_EXPANSION_PERMITTED,
split_levels,
)
class FlexFieldsSerializerMixin(object):
"""
A ModelSerializer that takes additional arguments for
"fields", "omit" and "expand" in order to
control which fields are displayed, and whether to replace simple
values with complex, nested serializations
"""
expandable_fields = {}
maximum_expansion_depth: Optional[int] = None
recursive_expansion_permitted: Optional[bool] = None
def __init__(self, *args, **kwargs):
expand = list(kwargs.pop(EXPAND_PARAM, []))
fields = list(kwargs.pop(FIELDS_PARAM, []))
omit = list(kwargs.pop(OMIT_PARAM, []))
parent = kwargs.pop("parent", None)
super(FlexFieldsSerializerMixin, self).__init__(*args, **kwargs)
self.parent = parent
self.expanded_fields = []
self._flex_fields_rep_applied = False
self._flex_options_base = {
"expand": expand,
"fields": fields,
"omit": omit,
}
self._flex_options_rep_only = {
"expand": (
self._get_permitted_expands_from_query_param(EXPAND_PARAM)
if not expand
else []
),
"fields": (self._get_query_param_value(FIELDS_PARAM) if not fields else []),
"omit": (self._get_query_param_value(OMIT_PARAM) if not omit else []),
}
self._flex_options_all = {
"expand": self._flex_options_base["expand"]
+ self._flex_options_rep_only["expand"],
"fields": self._flex_options_base["fields"]
+ self._flex_options_rep_only["fields"],
"omit": self._flex_options_base["omit"]
+ self._flex_options_rep_only["omit"],
}
def get_maximum_expansion_depth(self) -> Optional[int]:
"""
Defined at serializer level or based on MAXIMUM_EXPANSION_DEPTH setting
"""
return self.maximum_expansion_depth or MAXIMUM_EXPANSION_DEPTH
def get_recursive_expansion_permitted(self) -> bool:
"""
Defined at serializer level or based on RECURSIVE_EXPANSION_PERMITTED setting
"""
if self.recursive_expansion_permitted is not None:
return self.recursive_expansion_permitted
else:
return RECURSIVE_EXPANSION_PERMITTED
def to_representation(self, instance):
if not self._flex_fields_rep_applied:
self.apply_flex_fields(self.fields, self._flex_options_rep_only)
self._flex_fields_rep_applied = True
return super().to_representation(instance)
def get_fields(self):
fields = super().get_fields()
self.apply_flex_fields(fields, self._flex_options_base)
return fields
def apply_flex_fields(self, fields, flex_options):
expand_fields, next_expand_fields = split_levels(flex_options["expand"])
sparse_fields, next_sparse_fields = split_levels(flex_options["fields"])
omit_fields, next_omit_fields = split_levels(flex_options["omit"])
for field_name in self._get_fields_names_to_remove(
fields, omit_fields, sparse_fields, next_omit_fields
):
fields.pop(field_name)
for name in self._get_expanded_field_names(
expand_fields, omit_fields, sparse_fields, next_omit_fields
):
self.expanded_fields.append(name)
fields[name] = self._make_expanded_field_serializer(
name, next_expand_fields, next_sparse_fields, next_omit_fields
)
return fields
def _make_expanded_field_serializer(
self, name, nested_expand, nested_fields, nested_omit
):
"""
Returns an instance of the dynamically created nested serializer.
"""
field_options = self._expandable_fields[name]
if isinstance(field_options, tuple):
serializer_class = field_options[0]
settings = copy.deepcopy(field_options[1]) if len(field_options) > 1 else {}
else:
serializer_class = field_options
settings = {}
if type(serializer_class) == str:
serializer_class = self._get_serializer_class_from_lazy_string(
serializer_class
)
if issubclass(serializer_class, serializers.Serializer):
settings["context"] = self.context
if issubclass(serializer_class, FlexFieldsSerializerMixin):
settings["parent"] = self
if name in nested_expand:
settings[EXPAND_PARAM] = nested_expand[name]
if name in nested_fields:
settings[FIELDS_PARAM] = nested_fields[name]
if name in nested_omit:
settings[OMIT_PARAM] = nested_omit[name]
return serializer_class(**settings)
def _get_serializer_class_from_lazy_string(self, full_lazy_path: str):
path_parts = full_lazy_path.split(".")
class_name = path_parts.pop()
path = ".".join(path_parts)
serializer_class, error = self._import_serializer_class(path, class_name)
if error and not path.endswith(".serializers"):
serializer_class, error = self._import_serializer_class(
path + ".serializers", class_name
)
if serializer_class:
return serializer_class
raise Exception(error)
def _import_serializer_class(
self, path: str, class_name: str
) -> Tuple[Optional[str], Optional[str]]:
try:
module = importlib.import_module(path)
except ImportError:
return (
None,
"No module found at path: %s when trying to import %s"
% (path, class_name),
)
try:
return getattr(module, class_name), None
except AttributeError:
return None, "No class %s class found in module %s" % (path, class_name)
def _get_fields_names_to_remove(
self,
current_fields: List[str],
omit_fields: List[str],
sparse_fields: List[str],
next_level_omits: List[str],
) -> List[str]:
"""
Remove fields that are found in omit list, and if sparse names
are passed, remove any fields not found in that list.
"""
sparse = len(sparse_fields) > 0
to_remove = []
if not sparse and len(omit_fields) == 0:
return to_remove
for field_name in current_fields:
should_exist = self._should_field_exist(
field_name, omit_fields, sparse_fields, next_level_omits
)
if not should_exist:
to_remove.append(field_name)
return to_remove
def _should_field_exist(
self,
field_name: str,
omit_fields: List[str],
sparse_fields: List[str],
next_level_omits: List[str],
) -> bool:
"""
Next level omits take form of:
{
'this_level_field': [field_to_omit_at_next_level]
}
We don't want to prematurely omit a field, eg "omit=house.rooms.kitchen"
should not omit the entire house or all the rooms, just the kitchen.
"""
if field_name in omit_fields and field_name not in next_level_omits:
return False
elif self._contains_wildcard_value(sparse_fields):
return True
elif len(sparse_fields) > 0 and field_name not in sparse_fields:
return False
else:
return True
def _get_expanded_field_names(
self,
expand_fields: List[str],
omit_fields: List[str],
sparse_fields: List[str],
next_level_omits: List[str],
) -> List[str]:
if len(expand_fields) == 0:
return []
if self._contains_wildcard_value(expand_fields):
expand_fields = self._expandable_fields.keys()
accum = []
for name in expand_fields:
if name not in self._expandable_fields:
continue
if not self._should_field_exist(
name, omit_fields, sparse_fields, next_level_omits
):
continue
accum.append(name)
return accum
@property
def _expandable_fields(self) -> dict:
"""It's more consistent with DRF to declare the expandable fields
on the Meta class, however we need to support both places
for legacy reasons."""
if hasattr(self, "Meta") and hasattr(self.Meta, "expandable_fields"):
return self.Meta.expandable_fields
return self.expandable_fields
def _get_query_param_value(self, field: str) -> List[str]:
"""
Only allowed to examine query params if it's the root serializer.
"""
if self.parent:
return []
if not hasattr(self, "context") or not self.context.get("request"):
return []
values = self.context["request"].query_params.getlist(field)
if not values:
values = self.context["request"].query_params.getlist(f"{field}[]")
if values and len(values) == 1:
values = values[0].split(",")
for expand_path in values:
self._validate_recursive_expansion(expand_path)
self._validate_expansion_depth(expand_path)
return values or []
def _split_expand_field(self, expand_path: str) -> List[str]:
return expand_path.split(".")
def recursive_expansion_not_permitted(self):
"""
A customized exception can be raised when recursive expansion is found, default ValidationError
"""
raise serializers.ValidationError(detail="Recursive expansion found")
def _validate_recursive_expansion(self, expand_path: str) -> None:
"""
Given an expand_path, a dotted-separated string,
an Exception is raised when a recursive
expansion is detected.
Only applies when REST_FLEX_FIELDS["RECURSIVE_EXPANSION"] setting is False.
"""
recursive_expansion_permitted = self.get_recursive_expansion_permitted()
if recursive_expansion_permitted is True:
return
expansion_path = self._split_expand_field(expand_path)
expansion_length = len(expansion_path)
expansion_length_unique = len(set(expansion_path))
if expansion_length != expansion_length_unique:
self.recursive_expansion_not_permitted()
def expansion_depth_exceeded(self):
"""
A customized exception can be raised when expansion depth is found, default ValidationError
"""
raise serializers.ValidationError(detail="Expansion depth exceeded")
def _validate_expansion_depth(self, expand_path: str) -> None:
"""
Given an expand_path, a dotted-separated string,
an Exception is raised when expansion level is
greater than the `expansion_depth` property configuration.
Only applies when REST_FLEX_FIELDS["EXPANSION_DEPTH"] setting is set
or serializer has its own expansion configuration through default_expansion_depth attribute.
"""
maximum_expansion_depth = self.get_maximum_expansion_depth()
if maximum_expansion_depth is None:
return
expansion_path = self._split_expand_field(expand_path)
if len(expansion_path) > maximum_expansion_depth:
self.expansion_depth_exceeded()
def _get_permitted_expands_from_query_param(self, expand_param: str) -> List[str]:
"""
If a list of permitted_expands has been passed to context,
make sure that the "expand" fields from the query params
comply.
"""
expand = self._get_query_param_value(expand_param)
if "permitted_expands" in self.context:
permitted_expands = self.context["permitted_expands"]
if self._contains_wildcard_value(expand):
return permitted_expands
else:
return list(set(expand) & set(permitted_expands))
return expand
def _contains_wildcard_value(self, expand_values: List[str]) -> bool:
if WILDCARD_VALUES is None:
return False
intersecting_values = list(set(expand_values) & set(WILDCARD_VALUES))
return len(intersecting_values) > 0
class FlexFieldsModelSerializer(FlexFieldsSerializerMixin, serializers.ModelSerializer):
pass
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/rest_flex_fields/utils.py 0000664 0001750 0001750 00000004473 14400315017 021141 0 ustar 00robert robert from collections.abc import Iterable
from rest_flex_fields import EXPAND_PARAM, FIELDS_PARAM, OMIT_PARAM, WILDCARD_VALUES
def is_expanded(request, field: str) -> bool:
""" Examines request object to return boolean of whether
passed field is expanded.
"""
expand_value = request.query_params.get(EXPAND_PARAM)
expand_fields = []
if expand_value:
for f in expand_value.split(","):
expand_fields.extend([_ for _ in f.split(".")])
return any(field for field in expand_fields if field in WILDCARD_VALUES) or field in expand_fields
def is_included(request, field: str) -> bool:
""" Examines request object to return boolean of whether
passed field has been excluded, either because `fields` is
set, and it is not among them, or because `omit` is set and
it is among them.
"""
sparse_value = request.query_params.get(FIELDS_PARAM)
omit_value = request.query_params.get(OMIT_PARAM)
sparse_fields, omit_fields = [], []
if sparse_value:
for f in sparse_value.split(","):
sparse_fields.extend([_ for _ in f.split(".")])
if omit_value:
for f in omit_value.split(","):
omit_fields.extend([_ for _ in f.split(".")])
if len(sparse_fields) > 0 and field not in sparse_fields:
return False
if len(omit_fields) > 0 and field in omit_fields:
return False
return True
def split_levels(fields):
"""
Convert dot-notation such as ['a', 'a.b', 'a.d', 'c'] into
current-level fields ['a', 'c'] and next-level fields
{'a': ['b', 'd']}.
"""
first_level_fields = []
next_level_fields = {}
if not fields:
return first_level_fields, next_level_fields
assert isinstance(
fields, Iterable
), "`fields` must be iterable (e.g. list, tuple, or generator)"
if isinstance(fields, str):
fields = [a.strip() for a in fields.split(",") if a.strip()]
for e in fields:
if "." in e:
first_level, next_level = e.split(".", 1)
first_level_fields.append(first_level)
next_level_fields.setdefault(first_level, []).append(next_level)
else:
first_level_fields.append(e)
first_level_fields = list(set(first_level_fields))
return first_level_fields, next_level_fields
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/rest_flex_fields/views.py 0000664 0001750 0001750 00000001134 14400315017 021125 0 ustar 00robert robert """
This class helps provide control over which fields can be expanded when a
collection is request via the list method.
"""
from rest_framework import viewsets
class FlexFieldsMixin(object):
permit_list_expands = []
def get_serializer_context(self):
default_context = super(FlexFieldsMixin, self).get_serializer_context()
if hasattr(self, "action") and self.action == "list":
default_context["permitted_expands"] = self.permit_list_expands
return default_context
class FlexFieldsModelViewSet(FlexFieldsMixin, viewsets.ModelViewSet):
pass
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1678541408.2943482
drf-flex-fields-1.0.2/setup.cfg 0000664 0001750 0001750 00000000046 14403101140 015710 0 ustar 00robert robert [egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678541291.0
drf-flex-fields-1.0.2/setup.py 0000664 0001750 0001750 00000002426 14403100753 015616 0 ustar 00robert robert #!/usr/bin/env python
from setuptools import setup
from codecs import open
def readme():
with open("README.md", "r") as infile:
return infile.read()
classifiers = [
# Pick your license as you wish (should match "license" above)
"License :: OSI Approved :: MIT License",
# Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both.
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
]
setup(
name="drf-flex-fields",
version="1.0.2",
description="Flexible, dynamic fields and nested resources for Django REST Framework serializers.",
author="Robert Singer",
author_email="robertgsinger@gmail.com",
packages=["rest_flex_fields"],
url="https://github.com/rsinger86/drf-flex-fields",
license="MIT",
keywords="django rest api dynamic fields",
long_description=readme(),
classifiers=classifiers,
long_description_content_type="text/markdown",
)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1678541408.2903485
drf-flex-fields-1.0.2/tests/ 0000775 0001750 0001750 00000000000 14403101140 015231 5 ustar 00robert robert ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/tests/__init__.py 0000664 0001750 0001750 00000000000 14400315017 017337 0 ustar 00robert robert ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678540631.0
drf-flex-fields-1.0.2/tests/settings.py 0000664 0001750 0001750 00000006743 14403077527 017502 0 ustar 00robert robert """
Django settings for project project.
Generated by 'django-admin startproject' using Django 1.10.4.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "pn^^1@z0@4+kc*z-l93q4b#dav+_caec#!job^0#0v$f&8s8+e"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"tests.testapp",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "tests.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "project.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = "/static/"
REST_FLEX_FIELDS = {"EXPAND_PARAM": "expand"}
# In Django 3.2 and onwards, the primary keys are generated using `BigAutoField` instead
# of `AutoField`. To avoid introducing migrations and silence the configuration warnings,
# we're setting this to `AutoField`, which is ok for this use case (tests).
# Reference: https://docs.djangoproject.com/en/3.2/releases/3.2/#customizing-type-of-auto-created-primary-keys
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678540605.0
drf-flex-fields-1.0.2/tests/test_flex_fields_model_serializer.py 0000664 0001750 0001750 00000023756 14403077475 024603 0 ustar 00robert robert from unittest import TestCase
from unittest.mock import patch, PropertyMock
from django.test import override_settings
from django.utils.datastructures import MultiValueDict
from rest_framework import serializers
from rest_flex_fields import FlexFieldsModelSerializer
class MockRequest(object):
def __init__(self, query_params=None, method="GET"):
if query_params is None:
query_params = MultiValueDict()
self.query_params = query_params
self.method = method
class TestFlexFieldModelSerializer(TestCase):
def test_field_should_not_exist_if_omitted(self):
serializer = FlexFieldsModelSerializer()
result = serializer._should_field_exist("name", ["name"], [], {})
self.assertFalse(result)
def test_field_should_not_exist_if_not_in_sparse(self):
serializer = FlexFieldsModelSerializer()
result = serializer._should_field_exist("name", [], ["age"], {})
self.assertFalse(result)
def test_field_should_exist_if_ommitted_but_is_parent_of_omit(self):
serializer = FlexFieldsModelSerializer()
result = serializer._should_field_exist(
"employer", ["employer"], [], {"employer": ["address"]}
)
self.assertTrue(result)
def test_clean_fields(self):
serializer = FlexFieldsModelSerializer()
fields = {"cat": 1, "dog": 2, "zebra": 3}
result = serializer._get_fields_names_to_remove(fields, ["cat"], [], {})
self.assertEqual(result, ["cat"])
def test_get_expanded_field_names_if_all(self):
serializer = FlexFieldsModelSerializer()
serializer.expandable_fields = {"cat": "field", "dog": "field"}
result = serializer._get_expanded_field_names("*", [], [], {})
self.assertEqual(result, ["cat", "dog"])
def test_get_expanded_names_but_not_omitted(self):
serializer = FlexFieldsModelSerializer()
serializer.expandable_fields = {"cat": "field", "dog": "field"}
result = serializer._get_expanded_field_names(["cat", "dog"], ["cat"], [], {})
self.assertEqual(result, ["dog"])
def test_get_expanded_names_but_only_sparse(self):
serializer = FlexFieldsModelSerializer()
serializer.expandable_fields = {"cat": "field", "dog": "field"}
result = serializer._get_expanded_field_names(["cat"], [], ["cat"], {})
self.assertEqual(result, ["cat"])
def test_get_expanded_names_including_omitted_when_defer_to_next_level(self):
serializer = FlexFieldsModelSerializer()
serializer.expandable_fields = {"cat": "field", "dog": "field"}
result = serializer._get_expanded_field_names(
["cat"], ["cat"], [], {"cat": ["age"]}
)
self.assertEqual(result, ["cat"])
def test_get_query_param_value_should_return_empty_if_not_root_serializer(self):
serializer = FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET", query_params=MultiValueDict({"expand": ["cat"]})
)
},
)
serializer.parent = "Another serializer here"
self.assertFalse(serializer._get_query_param_value("expand"), [])
def test_get_omit_input_from_explicit_settings(self):
serializer = FlexFieldsModelSerializer(
omit=["fish"],
context={
"request": MockRequest(
method="GET", query_params=MultiValueDict({"omit": "cat,dog"})
)
},
)
self.assertEqual(serializer._flex_options_all["omit"], ["fish"])
def test_set_omit_input_from_query_param(self):
serializer = FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET", query_params=MultiValueDict({"omit": ["cat,dog"]})
)
}
)
self.assertEqual(serializer._flex_options_all["omit"], ["cat", "dog"])
def test_set_fields_input_from_explicit_settings(self):
serializer = FlexFieldsModelSerializer(
fields=["fish"],
context={
"request": MockRequest(
method="GET", query_params=MultiValueDict({"fields": "cat,dog"})
)
},
)
self.assertEqual(serializer._flex_options_all["fields"], ["fish"])
def test_set_fields_input_from_query_param(self):
serializer = FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET", query_params=MultiValueDict({"fields": ["cat,dog"]})
)
}
)
self.assertEqual(serializer._flex_options_all["fields"], ["cat", "dog"])
def test_set_expand_input_from_explicit_setting(self):
serializer = FlexFieldsModelSerializer(
fields=["cat"],
context={
"request": MockRequest(
method="GET", query_params=MultiValueDict({"fields": "cat,dog"})
)
},
)
self.assertEqual(serializer._flex_options_all["fields"], ["cat"])
def test_set_expand_input_from_query_param(self):
serializer = FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET", query_params=MultiValueDict({"expand": ["cat,dog"]})
)
}
)
self.assertEqual(serializer._flex_options_all["expand"], ["cat", "dog"])
def test_get_expand_input_from_query_param_limit_to_list_permitted(self):
serializer = FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET", query_params=MultiValueDict({"expand": ["cat,dog"]})
),
"permitted_expands": ["cat"],
}
)
self.assertEqual(serializer._flex_options_all["expand"], ["cat"])
def test_parse_request_list_value(self):
test_params = [
{"abc": ["cat,dog,mouse"]},
{"abc": ["cat", "dog", "mouse"]},
{"abc[]": ["cat", "dog", "mouse"]},
]
for query_params in test_params:
serializer = FlexFieldsModelSerializer(context={})
serializer.context["request"] = MockRequest(
method="GET", query_params=MultiValueDict(query_params)
)
result = serializer._get_query_param_value("abc")
self.assertEqual(result, ["cat", "dog", "mouse"])
def test_parse_request_list_value_empty_if_cannot_access_request(self):
serializer = FlexFieldsModelSerializer(context={})
result = serializer._get_query_param_value("abc")
self.assertEqual(result, [])
def test_import_serializer_class(self):
pass
def test_make_expanded_field_serializer(self):
pass
@patch("rest_flex_fields.serializers.RECURSIVE_EXPANSION_PERMITTED", False)
def test_recursive_expansion(self):
with self.assertRaises(serializers.ValidationError):
FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET",
query_params=MultiValueDict({"expand": ["dog.leg.dog"]}),
)
}
)
@patch(
"rest_flex_fields.FlexFieldsModelSerializer.recursive_expansion_permitted",
new_callable=PropertyMock,
)
def test_recursive_expansion_serializer_level(
self, mock_recursive_expansion_permitted
):
mock_recursive_expansion_permitted.return_value = False
with self.assertRaises(serializers.ValidationError):
FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET",
query_params=MultiValueDict({"expand": ["dog.leg.dog"]}),
)
}
)
@override_settings(REST_FLEX_FIELDS={"MAXIMUM_EXPANSION_DEPTH": 3})
def test_expansion_depth(self):
serializer = FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET",
query_params=MultiValueDict({"expand": ["dog.leg.paws"]}),
)
}
)
self.assertEqual(serializer._flex_options_all["expand"], ["dog.leg.paws"])
@patch("rest_flex_fields.serializers.MAXIMUM_EXPANSION_DEPTH", 2)
def test_expansion_depth_exception(self):
with self.assertRaises(serializers.ValidationError):
FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET",
query_params=MultiValueDict({"expand": ["dog.leg.paws"]}),
)
}
)
@patch(
"rest_flex_fields.FlexFieldsModelSerializer.maximum_expansion_depth",
new_callable=PropertyMock,
)
def test_expansion_depth_serializer_level(self, mock_maximum_expansion_depth):
mock_maximum_expansion_depth.return_value = 3
serializer = FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET",
query_params=MultiValueDict({"expand": ["dog.leg.paws"]}),
)
}
)
self.assertEqual(serializer._flex_options_all["expand"], ["dog.leg.paws"])
@patch(
"rest_flex_fields.FlexFieldsModelSerializer.maximum_expansion_depth",
new_callable=PropertyMock,
)
def test_expansion_depth_serializer_level_exception(
self, mock_maximum_expansion_depth
):
mock_maximum_expansion_depth.return_value = 2
with self.assertRaises(serializers.ValidationError):
FlexFieldsModelSerializer(
context={
"request": MockRequest(
method="GET",
query_params=MultiValueDict({"expand": ["dog.leg.paws"]}),
)
}
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1678540044.0
drf-flex-fields-1.0.2/tests/test_serializer.py 0000664 0001750 0001750 00000022227 14403076414 021037 0 ustar 00robert robert from unittest.mock import patch
from django.test import TestCase
from django.utils.datastructures import MultiValueDict
from rest_framework import serializers
from rest_flex_fields.serializers import FlexFieldsModelSerializer
from tests.testapp.models import Company, Person, Pet
from tests.testapp.serializers import PetSerializer
class MockRequest(object):
def __init__(self, query_params=None, method="GET"):
if query_params is None:
query_params = {}
self.query_params = query_params
self.method = method
class TestSerialize(TestCase):
def test_basic_field_omit(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(name="Fred"),
)
expected_serializer_data = {
"name": "Garfield",
"toys": "paper ball, string",
"diet": "",
"sold_from": None,
}
serializer = PetSerializer(pet, omit=["species", "owner"])
self.assertEqual(serializer.data, expected_serializer_data)
serializer = PetSerializer(pet, omit=(field for field in ("species", "owner")))
self.assertEqual(serializer.data, expected_serializer_data)
def test_nested_field_omit(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(name="Fred", employer=Company(name="McDonalds")),
)
expected_serializer_data = {
"diet": "",
"name": "Garfield",
"toys": "paper ball, string",
"species": "cat",
"owner": {"hobbies": "", "employer": {"name": "McDonalds"}},
"sold_from": None,
}
serializer = PetSerializer(
pet, expand=["owner.employer"], omit=["owner.name", "owner.employer.public"]
)
self.assertEqual(serializer.data, expected_serializer_data)
serializer = PetSerializer(
pet,
expand=(field for field in ("owner.employer",)),
omit=(field for field in ("owner.name", "owner.employer.public")),
)
self.assertEqual(serializer.data, expected_serializer_data)
def test_basic_field_include(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(name="Fred"),
)
expected_serializer_data = {"name": "Garfield", "toys": "paper ball, string"}
serializer = PetSerializer(pet, fields=["name", "toys"])
self.assertEqual(serializer.data, expected_serializer_data)
serializer = PetSerializer(pet, fields=(field for field in ("name", "toys")))
self.assertEqual(serializer.data, expected_serializer_data)
def test_nested_field_include(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(name="Fred", employer=Company(name="McDonalds")),
)
expected_serializer_data = {"owner": {"employer": {"name": "McDonalds"}}}
serializer = PetSerializer(
pet, expand=["owner.employer"], fields=["owner.employer.name"]
)
self.assertEqual(serializer.data, expected_serializer_data)
serializer = PetSerializer(
pet,
expand=(field for field in ("owner.employer",)),
fields=(field for field in ("owner.employer.name",)),
)
self.assertEqual(serializer.data, expected_serializer_data)
def test_basic_expand(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(name="Fred", hobbies="sailing"),
)
expected_serializer_data = {
"name": "Garfield",
"toys": "paper ball, string",
"species": "cat",
"owner": {"name": "Fred", "hobbies": "sailing"},
"sold_from": None,
"diet": "",
}
request = MockRequest(query_params=MultiValueDict({"expand": ["owner"]}))
serializer = PetSerializer(pet, context={"request": request})
self.assertEqual(serializer.data, expected_serializer_data)
self.assertEqual(serializer.fields["owner"].context.get("request"), request)
serializer = PetSerializer(pet, expand=(field for field in ("owner",)))
self.assertEqual(serializer.data, expected_serializer_data)
def test_nested_expand(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(
name="Fred", hobbies="sailing", employer=Company(name="McDonalds")
),
)
expected_serializer_data = {
"diet": "",
"name": "Garfield",
"toys": "paper ball, string",
"species": "cat",
"owner": {
"name": "Fred",
"hobbies": "sailing",
"employer": {"public": False, "name": "McDonalds"},
},
"sold_from": None,
}
request = MockRequest(
query_params=MultiValueDict({"expand": ["owner.employer"]})
)
serializer = PetSerializer(pet, context={"request": request})
self.assertEqual(serializer.data, expected_serializer_data)
self.assertEqual(
serializer.fields["owner"].fields["employer"].context.get("request"),
request,
)
serializer = PetSerializer(pet, expand=(field for field in ("owner.employer",)))
self.assertEqual(serializer.data, expected_serializer_data)
def test_expand_from_request(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(
name="Fred", hobbies="sailing", employer=Company(name="McDonalds")
),
)
request = MockRequest(
query_params=MultiValueDict({"expand": ["owner.employer"]})
)
serializer = PetSerializer(pet, context={"request": request})
self.assertEqual(
serializer.data,
{
"diet": "",
"name": "Garfield",
"toys": "paper ball, string",
"species": "cat",
"sold_from": None,
"owner": {
"name": "Fred",
"hobbies": "sailing",
"employer": {"public": False, "name": "McDonalds"},
},
},
)
@patch("rest_flex_fields.serializers.EXPAND_PARAM", "include")
def test_expand_with_custom_param_name(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(name="Fred", hobbies="sailing"),
)
expected_serializer_data = {
"diet": "",
"name": "Garfield",
"toys": "paper ball, string",
"species": "cat",
"owner": {"name": "Fred", "hobbies": "sailing"},
"sold_from": None,
}
serializer = PetSerializer(pet, include=["owner"])
self.assertEqual(serializer.data, expected_serializer_data)
@patch("rest_flex_fields.serializers.OMIT_PARAM", "exclude")
def test_omit_with_custom_param_name(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(name="Fred"),
)
expected_serializer_data = {
"name": "Garfield",
"toys": "paper ball, string",
"diet": "",
"sold_from": None,
}
serializer = PetSerializer(pet, exclude=["species", "owner"])
self.assertEqual(serializer.data, expected_serializer_data)
@patch("rest_flex_fields.serializers.FIELDS_PARAM", "only")
def test_fields_include_with_custom_param_name(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(name="Fred"),
)
expected_serializer_data = {"name": "Garfield", "toys": "paper ball, string"}
serializer = PetSerializer(pet, only=["name", "toys"])
self.assertEqual(serializer.data, expected_serializer_data)
def test_all_special_value_in_serialize(self):
pet = Pet(
name="Garfield",
toys="paper ball, string",
species="cat",
owner=Person(name="Fred", employer=Company(name="McDonalds")),
)
class PetSerializer(FlexFieldsModelSerializer):
owner = serializers.PrimaryKeyRelatedField(
queryset=Person.objects.all(), allow_null=True
)
class Meta:
model = Pet
fields = "__all__"
serializer = PetSerializer(
fields=("name", "toys"),
data={
"name": "Garfield",
"toys": "paper ball",
"species": "cat",
"owner": None,
"diet": "lasanga",
},
)
serializer.is_valid(raise_exception=True)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/tests/test_utils.py 0000664 0001750 0001750 00000003664 14400315017 020022 0 ustar 00robert robert from django.test import TestCase
from rest_flex_fields import is_included, is_expanded, WILDCARD_ALL, WILDCARD_ASTERISK
class MockRequest(object):
def __init__(self, query_params=None, method="GET"):
if query_params is None:
query_params = {}
self.query_params = query_params
self.method = method
class TestUtils(TestCase):
def test_should_be_included(self):
request = MockRequest(query_params={})
self.assertTrue(is_included(request, "name"))
def test_should_not_be_included(self):
request = MockRequest(query_params={"omit": "name,address"})
self.assertFalse(is_included(request, "name"))
def test_should_not_be_included_and_due_to_omit_and_has_dot_notation(self):
request = MockRequest(query_params={"omit": "friend.name,address"})
self.assertFalse(is_included(request, "name"))
def test_should_not_be_included_and_due_to_fields_and_has_dot_notation(self):
request = MockRequest(query_params={"fields": "hobby,address"})
self.assertFalse(is_included(request, "name"))
def test_should_be_expanded(self):
request = MockRequest(query_params={"expand": "name,address"})
self.assertTrue(is_expanded(request, "name"))
def test_should_not_be_expanded(self):
request = MockRequest(query_params={"expand": "name,address"})
self.assertFalse(is_expanded(request, "hobby"))
def test_should_be_expanded_and_has_dot_notation(self):
request = MockRequest(query_params={"expand": "person.name,address"})
self.assertTrue(is_expanded(request, "name"))
def test_all_should_be_expanded(self):
request = MockRequest(query_params={"expand": WILDCARD_ALL})
self.assertTrue(is_expanded(request, "name"))
def test_asterisk_should_be_expanded(self):
request = MockRequest(query_params={"expand": WILDCARD_ASTERISK})
self.assertTrue(is_expanded(request, "name"))
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/tests/test_views.py 0000664 0001750 0001750 00000023555 14400315017 020020 0 ustar 00robert robert from http import HTTPStatus
from pprint import pprint
from unittest.mock import patch
from django.contrib.contenttypes.models import ContentType
from django.db import connection
from django.test import override_settings
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_flex_fields.filter_backends import FlexFieldsFilterBackend
from tests.testapp.models import Company, Person, Pet, PetStore, TaggedItem
class PetViewTests(APITestCase):
def setUp(self):
self.company = Company.objects.create(name="McDonalds")
self.person = Person.objects.create(
name="Fred", hobbies="sailing", employer=self.company
)
self.pet = Pet.objects.create(
name="Garfield", toys="paper ball, string", species="cat", owner=self.person
)
def tearDown(self):
Company.objects.all().delete()
Person.objects.all().delete()
Pet.objects.all().delete()
def test_retrieve_expanded(self):
url = reverse("pet-detail", args=[self.pet.id])
response = self.client.get(url + "?expand=owner", format="json")
self.assertEqual(
response.data,
{
"diet": "",
"name": "Garfield",
"toys": "paper ball, string",
"species": "cat",
"sold_from": None,
"owner": {"name": "Fred", "hobbies": "sailing"},
},
)
def test_retrieve_sparse(self):
url = reverse("pet-detail", args=[self.pet.id])
response = self.client.get(url + "?fields=name,species", format="json")
self.assertEqual(response.data, {"name": "Garfield", "species": "cat"})
def test_retrieve_sparse_and_deep_expanded(self):
url = reverse("pet-detail", args=[self.pet.id])
url = url + "?fields=owner&expand=owner.employer"
response = self.client.get(url, format="json")
self.assertEqual(
response.data,
{
"owner": {
"name": "Fred",
"hobbies": "sailing",
"employer": {"public": False, "name": "McDonalds"},
}
},
)
def test_retrieve_all_fields_at_root_and_sparse_fields_at_next_level(self):
url = reverse("pet-detail", args=[self.pet.id])
url = url + "?fields=*,owner.name&expand=owner"
response = self.client.get(url, format="json")
self.assertEqual(
response.data,
{
"name": "Garfield",
"toys": "paper ball, string",
"species": "cat",
"diet": "",
"sold_from": None,
"owner": {
"name": "Fred",
},
},
)
def test_list_expanded(self):
url = reverse("pet-list")
url = url + "?expand=owner"
response = self.client.get(url, format="json")
self.assertEqual(
response.data[0],
{
"diet": "",
"name": "Garfield",
"toys": "paper ball, string",
"species": "cat",
"sold_from": None,
"owner": {"name": "Fred", "hobbies": "sailing"},
},
)
def test_create_and_return_expanded_field(self):
url = reverse("pet-list")
url = url + "?expand=owner"
response = self.client.post(
url,
{
"diet": "rats",
"owner": self.person.id,
"species": "snake",
"toys": "playstation",
"name": "Freddy",
"sold_from": None,
},
format="json",
)
self.assertEqual(
response.data,
{
"name": "Freddy",
"diet": "rats",
"toys": "playstation",
"sold_from": None,
"species": "snake",
"owner": {"name": "Fred", "hobbies": "sailing"},
},
)
def test_expand_drf_serializer_field(self):
url = reverse("pet-detail", args=[self.pet.id])
response = self.client.get(url + "?expand=diet", format="json")
self.assertEqual(
response.data,
{
"diet": "homemade lasanga",
"name": "Garfield",
"toys": "paper ball, string",
"sold_from": None,
"species": "cat",
"owner": self.pet.owner_id,
},
)
def test_expand_drf_model_serializer(self):
petco = PetStore.objects.create(name="PetCo")
self.pet.sold_from = petco
self.pet.save()
url = reverse("pet-detail", args=[self.pet.id])
response = self.client.get(url + "?expand=sold_from", format="json")
self.assertEqual(
response.data,
{
"diet": "",
"name": "Garfield",
"toys": "paper ball, string",
"sold_from": {"id": petco.id, "name": "PetCo"},
"species": "cat",
"owner": self.pet.owner_id,
},
)
@override_settings(DEBUG=True)
@patch("tests.testapp.views.PetViewSet.filter_backends", [FlexFieldsFilterBackend])
class PetViewWithSelectFieldsFilterBackendTests(PetViewTests):
def test_query_optimization(self):
url = reverse("pet-list")
url = url + "?expand=owner&fields=name,owner"
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(len(connection.queries), 1)
self.assertEqual(
connection.queries[0]["sql"],
(
"SELECT "
'"testapp_pet"."id", '
'"testapp_pet"."name", '
'"testapp_pet"."owner_id", '
'"testapp_person"."id", '
'"testapp_person"."name", '
'"testapp_person"."hobbies", '
'"testapp_person"."employer_id" '
'FROM "testapp_pet" '
'INNER JOIN "testapp_person" ON ("testapp_pet"."owner_id" = "testapp_person"."id")'
),
)
# todo: test many to one
# todo: test many to many
# todo: test view options for SelectFieldsFilterBackend
@override_settings(DEBUG=True)
@patch("tests.testapp.views.TaggedItemViewSet.filter_backends", [FlexFieldsFilterBackend])
class TaggedItemViewWithSelectFieldsFilterBackendTests(APITestCase):
def test_query_optimization_includes_generic_foreign_keys_in_prefetch_related(self):
self.company = Company.objects.create(name="McDonalds")
self.person = Person.objects.create(
name="Fred", hobbies="sailing", employer=self.company
)
self.pet1 = Pet.objects.create(
name="Garfield", toys="paper ball, string", species="cat",
owner=self.person
)
self.pet2 = Pet.objects.create(
name="Garfield", toys="paper ball, string", species="cat",
owner=self.person
)
self.tagged_item1 = TaggedItem.objects.create(
content_type=ContentType.objects.get_for_model(Pet),
object_id=self.pet1.id
)
self.tagged_item2 = TaggedItem.objects.create(
content_type=ContentType.objects.get_for_model(Pet),
object_id=self.pet2.id
)
self.tagged_item3 = TaggedItem.objects.create(
content_type=ContentType.objects.get_for_model(Person),
object_id=self.person.id
)
self.tagged_item4 = TaggedItem.objects.create(
content_type=ContentType.objects.get_for_model(Company),
object_id=self.company.id
)
url = reverse("tagged-item-list")
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(len(connection.queries), 4)
self.assertEqual(
connection.queries[0]["sql"],
(
'SELECT '
'"testapp_taggeditem"."id", '
'"testapp_taggeditem"."content_type_id", '
'"testapp_taggeditem"."object_id", '
'"django_content_type"."id", '
'"django_content_type"."app_label", '
'"django_content_type"."model" '
'FROM "testapp_taggeditem" '
'INNER JOIN "django_content_type" ON ("testapp_taggeditem"."content_type_id" = "django_content_type"."id")'
))
self.assertEqual(
connection.queries[1]["sql"],
(
'SELECT '
'"testapp_pet"."id", '
'"testapp_pet"."name", '
'"testapp_pet"."toys", '
'"testapp_pet"."species", '
'"testapp_pet"."owner_id", '
'"testapp_pet"."sold_from_id", '
'"testapp_pet"."diet" '
'FROM "testapp_pet" WHERE "testapp_pet"."id" IN ({0}, {1})'.format(self.pet1.id, self.pet2.id)
)
)
self.assertEqual(
connection.queries[2]["sql"],
(
'SELECT '
'"testapp_person"."id", '
'"testapp_person"."name", '
'"testapp_person"."hobbies", '
'"testapp_person"."employer_id" '
'FROM "testapp_person" WHERE "testapp_person"."id" IN ({0})'.format(self.person.id)
)
)
self.assertEqual(
connection.queries[3]["sql"],
(
'SELECT '
'"testapp_company"."id", '
'"testapp_company"."name", '
'"testapp_company"."public" '
'FROM "testapp_company" WHERE "testapp_company"."id" IN ({0})'.format(self.company.id)
)
)
self.assertEqual(len(response.json()), 4) ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1678541408.2943482
drf-flex-fields-1.0.2/tests/testapp/ 0000775 0001750 0001750 00000000000 14403101140 016711 5 ustar 00robert robert ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/tests/testapp/__init__.py 0000664 0001750 0001750 00000000000 14400315017 021017 0 ustar 00robert robert ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/tests/testapp/apps.py 0000664 0001750 0001750 00000000137 14400315017 020236 0 ustar 00robert robert from django.apps import AppConfig
class TestappConfig(AppConfig):
name = 'tests.testapp'
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/tests/testapp/models.py 0000664 0001750 0001750 00000002232 14400315017 020554 0 ustar 00robert robert from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
class Company(models.Model):
name = models.CharField(max_length=30)
public = models.BooleanField(default=False)
class PetStore(models.Model):
name = models.CharField(max_length=30)
class Person(models.Model):
name = models.CharField(max_length=30)
hobbies = models.CharField(max_length=30)
employer = models.ForeignKey(Company, on_delete=models.CASCADE)
class Pet(models.Model):
name = models.CharField(max_length=30)
toys = models.CharField(max_length=30)
species = models.CharField(max_length=30)
owner = models.ForeignKey(Person, on_delete=models.CASCADE)
sold_from = models.ForeignKey(PetStore, null=True, on_delete=models.CASCADE)
diet = models.CharField(max_length=200)
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') ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/tests/testapp/serializers.py 0000664 0001750 0001750 00000003301 14400315017 021623 0 ustar 00robert robert from rest_framework import serializers
from rest_framework.relations import PrimaryKeyRelatedField
from rest_flex_fields import FlexFieldsModelSerializer
from tests.testapp.models import Pet, PetStore, Person, Company, TaggedItem
class CompanySerializer(FlexFieldsModelSerializer):
class Meta:
model = Company
fields = ["name", "public"]
class PersonSerializer(FlexFieldsModelSerializer):
class Meta:
model = Person
fields = ["name", "hobbies"]
expandable_fields = {"employer": "tests.testapp.serializers.CompanySerializer"}
class PetStoreSerializer(serializers.ModelSerializer):
class Meta:
model = PetStore
fields = ["id", "name"]
class PetSerializer(FlexFieldsModelSerializer):
owner = serializers.PrimaryKeyRelatedField(queryset=Person.objects.all())
sold_from = serializers.PrimaryKeyRelatedField(
queryset=PetStore.objects.all(), allow_null=True
)
diet = serializers.CharField()
class Meta:
model = Pet
fields = ["owner", "name", "toys", "species", "diet", "sold_from"]
expandable_fields = {
"owner": "tests.testapp.PersonSerializer",
"sold_from": "tests.testapp.PetStoreSerializer",
"diet": serializers.SerializerMethodField,
}
def get_diet(self, obj):
if obj.name == "Garfield":
return "homemade lasanga"
return "pet food"
class TaggedItemSerializer(FlexFieldsModelSerializer):
content_object = PrimaryKeyRelatedField(read_only=True)
class Meta:
model = TaggedItem
fields = (
"id",
"content_type",
"object_id",
"content_object"
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/tests/testapp/views.py 0000664 0001750 0001750 00000001054 14400315017 020427 0 ustar 00robert robert from rest_framework.viewsets import ModelViewSet
from rest_flex_fields import FlexFieldsModelViewSet
from tests.testapp.models import Pet, TaggedItem
from tests.testapp.serializers import PetSerializer, TaggedItemSerializer
class PetViewSet(FlexFieldsModelViewSet):
"""
API endpoint for testing purposes.
"""
serializer_class = PetSerializer
queryset = Pet.objects.all()
permit_list_expands = ["owner"]
class TaggedItemViewSet(ModelViewSet):
serializer_class = TaggedItemSerializer
queryset = TaggedItem.objects.all()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1677826575.0
drf-flex-fields-1.0.2/tests/urls.py 0000664 0001750 0001750 00000000563 14400315017 016603 0 ustar 00robert robert from django.conf.urls import url, include
from rest_framework import routers
from tests.testapp.views import PetViewSet, TaggedItemViewSet
# Standard viewsets
router = routers.DefaultRouter()
router.register(r"pets", PetViewSet, basename="pet")
router.register(r"tagged-items", TaggedItemViewSet, basename="tagged-item")
urlpatterns = [url(r"^", include(router.urls))]