././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1641592746.2746706 drf-flex-fields-0.9.7/0000775000175000017500000000000000000000000013773 5ustar00robbierobbie././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629056374.0 drf-flex-fields-0.9.7/LICENSE0000664000175000017500000000205200000000000014777 0ustar00robbierobbieMIT 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=1629056374.0 drf-flex-fields-0.9.7/MANIFEST.in0000664000175000017500000000012200000000000015524 0ustar00robbierobbieinclude LICENSE include README.md include README.txt recursive-include tests *.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1641592746.2706707 drf-flex-fields-0.9.7/PKG-INFO0000664000175000017500000006702100000000000015076 0ustar00robbierobbieMetadata-Version: 2.1 Name: drf-flex-fields Version: 0.9.7 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 Description: # 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"` | | 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"` | | 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"} ``` ## 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 ## 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). 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641592629.0 drf-flex-fields-0.9.7/README.md0000664000175000017500000005274500000000000015267 0ustar00robbierobbie# 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"` | | 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"` | | 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"} ``` ## 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 ## 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=1629056374.0 drf-flex-fields-0.9.7/README.txt0000664000175000017500000004026000000000000015473 0ustar00robbierobbieDjango 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=1641592746.2666707 drf-flex-fields-0.9.7/drf_flex_fields.egg-info/0000775000175000017500000000000000000000000020604 5ustar00robbierobbie././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641592746.0 drf-flex-fields-0.9.7/drf_flex_fields.egg-info/PKG-INFO0000664000175000017500000006702100000000000021707 0ustar00robbierobbieMetadata-Version: 2.1 Name: drf-flex-fields Version: 0.9.7 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 Description: # 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"` | | 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"` | | 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"} ``` ## 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 ## 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). 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641592746.0 drf-flex-fields-0.9.7/drf_flex_fields.egg-info/SOURCES.txt0000664000175000017500000000117400000000000022473 0ustar00robbierobbieLICENSE 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=1641592746.0 drf-flex-fields-0.9.7/drf_flex_fields.egg-info/dependency_links.txt0000664000175000017500000000000100000000000024652 0ustar00robbierobbie ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641592746.0 drf-flex-fields-0.9.7/drf_flex_fields.egg-info/top_level.txt0000664000175000017500000000002100000000000023327 0ustar00robbierobbierest_flex_fields ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1641592746.2706707 drf-flex-fields-0.9.7/rest_flex_fields/0000775000175000017500000000000000000000000017314 5ustar00robbierobbie././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636926512.0 drf-flex-fields-0.9.7/rest_flex_fields/__init__.py0000664000175000017500000000200200000000000021417 0ustar00robbierobbiefrom 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") 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 = ["~all", "*"] 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, None): raise ValueError("'WILDCARD_EXPAND_VALUES' should be a list of strings or None") from .utils import * from .serializers import FlexFieldsModelSerializer from .views import FlexFieldsModelViewSet ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641592347.0 drf-flex-fields-0.9.7/rest_flex_fields/filter_backends.py0000664000175000017500000001366700000000000023022 0ustar00robbierobbiefrom 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.serializers import FlexFieldsSerializerMixin class FlexFieldsFilterBackend(BaseFilterBackend): 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) ): 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 ] ) ) 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 ) ) 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 ) ) 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 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()`" if not issubclass(view.get_serializer_class(), FlexFieldsSerializerMixin): return [] return [ coreapi.Field( name="fields", required=False, location="query", schema=coreschema.String( title="Selected fields", description="Specify required field by comma", ), example="field1,field2,nested.field", ), coreapi.Field( name="omit", required=False, location="query", schema=coreschema.String( title="Omitted fields", description="Specify required field by comma", ), example="field1,field2,nested.field", ), coreapi.Field( name="expand", required=False, location="query", schema=coreschema.String( title="Expanded fields", description="Specify required nested items by comma", ), example="nested1,nested2", ), ] def get_schema_operation_parameters(self, view): if not issubclass(view.get_serializer_class(), FlexFieldsSerializerMixin): return [] parameters = [ { "name": "fields", "required": False, "in": "query", "description": "Specify required field by comma", "schema": { "title": "Selected fields", "type": "string", }, "example": "field1,field2,nested.field", }, { "name": "omit", "required": False, "in": "query", "description": "Specify required field by comma", "schema": { "title": "Omitted fields", "type": "string", }, "example": "field1,field2,nested.field", }, { "name": "expand", "required": False, "in": "query", "description": "Specify required field by comma", "schema": { "title": "Expanded fields", "type": "string", }, "example": "nested1,nested2", }, ] return parameters ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636926512.0 drf-flex-fields-0.9.7/rest_flex_fields/serializers.py0000664000175000017500000002311300000000000022222 0ustar00robbierobbieimport 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, 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 = {} 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 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("{}[]".format(field)) if values and len(values) == 1: return values[0].split(",") return values or [] 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=1629056374.0 drf-flex-fields-0.9.7/rest_flex_fields/utils.py0000664000175000017500000000440000000000000021024 0ustar00robbierobbiefrom collections.abc import Iterable from rest_flex_fields import EXPAND_PARAM, FIELDS_PARAM, OMIT_PARAM 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 "~all" in expand_fields 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=1629056374.0 drf-flex-fields-0.9.7/rest_flex_fields/views.py0000664000175000017500000000113400000000000021022 0ustar00robbierobbie""" 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=1641592746.2746706 drf-flex-fields-0.9.7/setup.cfg0000664000175000017500000000004600000000000015614 0ustar00robbierobbie[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641592720.0 drf-flex-fields-0.9.7/setup.py0000664000175000017500000000242600000000000015511 0ustar00robbierobbie#!/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="0.9.7", 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=1641592746.2706707 drf-flex-fields-0.9.7/tests/0000775000175000017500000000000000000000000015135 5ustar00robbierobbie././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629056374.0 drf-flex-fields-0.9.7/tests/__init__.py0000664000175000017500000000000000000000000017234 0ustar00robbierobbie././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635712144.0 drf-flex-fields-0.9.7/tests/settings.py0000664000175000017500000000667100000000000017361 0ustar00robbierobbie""" 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=1636926512.0 drf-flex-fields-0.9.7/tests/test_flex_fields_model_serializer.py0000664000175000017500000001512000000000000024442 0ustar00robbierobbiefrom unittest import TestCase from django.utils.datastructures import MultiValueDict from rest_flex_fields import FlexFieldsModelSerializer class MockRequest(object): def __init__(self, query_params=MultiValueDict(), method="GET"): 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636926512.0 drf-flex-fields-0.9.7/tests/test_serializer.py0000664000175000017500000002212600000000000020722 0ustar00robbierobbiefrom 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={}, method="GET"): 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=1629056374.0 drf-flex-fields-0.9.7/tests/test_utils.py0000664000175000017500000000277200000000000017716 0ustar00robbierobbiefrom django.test import TestCase from rest_flex_fields import is_included, is_expanded class MockRequest(object): def __init__(self, query_params={}, method="GET"): 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")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636926512.0 drf-flex-fields-0.9.7/tests/test_views.py0000664000175000017500000001436300000000000017712 0ustar00robbierobbiefrom http import HTTPStatus from pprint import pprint from unittest.mock import patch 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 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1641592746.2706707 drf-flex-fields-0.9.7/tests/testapp/0000775000175000017500000000000000000000000016615 5ustar00robbierobbie././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629056374.0 drf-flex-fields-0.9.7/tests/testapp/__init__.py0000664000175000017500000000000000000000000020714 0ustar00robbierobbie././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635712144.0 drf-flex-fields-0.9.7/tests/testapp/apps.py0000664000175000017500000000013700000000000020133 0ustar00robbierobbiefrom django.apps import AppConfig class TestappConfig(AppConfig): name = 'tests.testapp' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629058325.0 drf-flex-fields-0.9.7/tests/testapp/models.py0000664000175000017500000000144100000000000020452 0ustar00robbierobbiefrom __future__ import unicode_literals 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636926512.0 drf-flex-fields-0.9.7/tests/testapp/serializers.py0000664000175000017500000000252600000000000021530 0ustar00robbierobbiefrom rest_framework import serializers from rest_flex_fields import FlexFieldsModelSerializer from tests.testapp.models import Pet, PetStore, Person, Company 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" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629056374.0 drf-flex-fields-0.9.7/tests/testapp/views.py0000664000175000017500000000053300000000000020325 0ustar00robbierobbiefrom rest_flex_fields import FlexFieldsModelViewSet from tests.testapp.models import Pet from tests.testapp.serializers import PetSerializer class PetViewSet(FlexFieldsModelViewSet): """ API endpoint for testing purposes. """ serializer_class = PetSerializer queryset = Pet.objects.all() permit_list_expands = ["owner"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1629056374.0 drf-flex-fields-0.9.7/tests/urls.py0000664000175000017500000000042500000000000016475 0ustar00robbierobbiefrom django.conf.urls import url, include from rest_framework import routers from tests.testapp.views import PetViewSet # Standard viewsets router = routers.DefaultRouter() router.register(r"pets", PetViewSet, basename="pet") urlpatterns = [url(r"^", include(router.urls))]