././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1678541408.2943482 drf-flex-fields-1.0.2/0000775000175000017500000000000014403101140014067 5ustar00robertrobert././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/LICENSE0000664000175000017500000000205214400315017015102 0ustar00robertrobertMIT 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/MANIFEST.in0000664000175000017500000000012214400315017015627 0ustar00robertrobertinclude LICENSE include README.md include README.txt recursive-include tests *.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1678541408.2943482 drf-flex-fields-1.0.2/PKG-INFO0000664000175000017500000006533614403101140015201 0ustar00robertrobertMetadata-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 [![Package version](https://badge.fury.io/py/drf-flex-fields.svg)](https://pypi.python.org/pypi/drf-flex-fields) [![Python versions](https://img.shields.io/pypi/status/drf-flex-fields.svg)](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). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678541268.0 drf-flex-fields-1.0.2/README.md0000664000175000017500000006360714403100724015371 0ustar00robertrobert# Django REST - FlexFields [![Package version](https://badge.fury.io/py/drf-flex-fields.svg)](https://pypi.python.org/pypi/drf-flex-fields) [![Python versions](https://img.shields.io/pypi/status/drf-flex-fields.svg)](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). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/README.txt0000664000175000017500000004026014400315017015576 0ustar00robertrobertDjango 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 `__. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1678541408.2903485 drf-flex-fields-1.0.2/drf_flex_fields.egg-info/0000775000175000017500000000000014403101140020700 5ustar00robertrobert././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678541408.0 drf-flex-fields-1.0.2/drf_flex_fields.egg-info/PKG-INFO0000664000175000017500000006533614403101140022012 0ustar00robertrobertMetadata-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 [![Package version](https://badge.fury.io/py/drf-flex-fields.svg)](https://pypi.python.org/pypi/drf-flex-fields) [![Python versions](https://img.shields.io/pypi/status/drf-flex-fields.svg)](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). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678541408.0 drf-flex-fields-1.0.2/drf_flex_fields.egg-info/SOURCES.txt0000664000175000017500000000117414403101140022567 0ustar00robertrobertLICENSE 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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678541408.0 drf-flex-fields-1.0.2/drf_flex_fields.egg-info/dependency_links.txt0000664000175000017500000000000114403101140024746 0ustar00robertrobert ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678541408.0 drf-flex-fields-1.0.2/drf_flex_fields.egg-info/top_level.txt0000664000175000017500000000002114403101140023423 0ustar00robertrobertrest_flex_fields ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1678541408.2903485 drf-flex-fields-1.0.2/rest_flex_fields/0000775000175000017500000000000014403101140017410 5ustar00robertrobert././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678540543.0 drf-flex-fields-1.0.2/rest_flex_fields/__init__.py0000664000175000017500000000300414403077377021546 0ustar00robertrobertfrom 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/rest_flex_fields/filter_backends.py0000664000175000017500000002131114400315017023106 0ustar00robertrobertfrom 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678540855.0 drf-flex-fields-1.0.2/rest_flex_fields/serializers.py0000664000175000017500000003077314403100067022337 0ustar00robertrobertimport 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/rest_flex_fields/utils.py0000664000175000017500000000447314400315017021141 0ustar00robertrobertfrom 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/rest_flex_fields/views.py0000664000175000017500000000113414400315017021125 0ustar00robertrobert""" 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1678541408.2943482 drf-flex-fields-1.0.2/setup.cfg0000664000175000017500000000004614403101140015710 0ustar00robertrobert[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678541291.0 drf-flex-fields-1.0.2/setup.py0000664000175000017500000000242614403100753015616 0ustar00robertrobert#!/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", ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1678541408.2903485 drf-flex-fields-1.0.2/tests/0000775000175000017500000000000014403101140015231 5ustar00robertrobert././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/tests/__init__.py0000664000175000017500000000000014400315017017337 0ustar00robertrobert././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678540631.0 drf-flex-fields-1.0.2/tests/settings.py0000664000175000017500000000674314403077527017502 0ustar00robertrobert""" 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" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678540605.0 drf-flex-fields-1.0.2/tests/test_flex_fields_model_serializer.py0000664000175000017500000002375614403077475024603 0ustar00robertrobertfrom 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"]}), ) } ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1678540044.0 drf-flex-fields-1.0.2/tests/test_serializer.py0000664000175000017500000002222714403076414021037 0ustar00robertrobertfrom 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/tests/test_utils.py0000664000175000017500000000366414400315017020022 0ustar00robertrobertfrom 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")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/tests/test_views.py0000664000175000017500000002355514400315017020020 0ustar00robertrobertfrom 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)././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1678541408.2943482 drf-flex-fields-1.0.2/tests/testapp/0000775000175000017500000000000014403101140016711 5ustar00robertrobert././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/tests/testapp/__init__.py0000664000175000017500000000000014400315017021017 0ustar00robertrobert././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/tests/testapp/apps.py0000664000175000017500000000013714400315017020236 0ustar00robertrobertfrom django.apps import AppConfig class TestappConfig(AppConfig): name = 'tests.testapp' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/tests/testapp/models.py0000664000175000017500000000223214400315017020554 0ustar00robertrobertfrom __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')././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/tests/testapp/serializers.py0000664000175000017500000000330114400315017021623 0ustar00robertrobertfrom 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" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/tests/testapp/views.py0000664000175000017500000000105414400315017020427 0ustar00robertrobertfrom 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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1677826575.0 drf-flex-fields-1.0.2/tests/urls.py0000664000175000017500000000056314400315017016603 0ustar00robertrobertfrom 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))]