GithubHelp home page GithubHelp logo

rsinger86 / drf-flex-fields Goto Github PK

View Code? Open in Web Editor NEW
712.0 12.0 61.0 243 KB

Dynamically set fields and expand nested resources in Django REST Framework serializers.

License: MIT License

Python 100.00%
django-rest-framework field-expansion django

drf-flex-fields's Introduction

Django REST - FlexFields

Package version Python versions

Flexible, dynamic fields and nested models for Django REST Framework serializers.

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. 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

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/
{
  "id": 142,
  "name": "Jim Halpert",
  "country": 1
}
GET /people/142/?expand=country.states
{
  "id": 142,
  "name": "Jim Halpert",
  "country": {
    "id": 1,
    "name": "United States",
    "states": [
      {
        "id": 23,
        "name": "Ohio"
      },
      {
        "id": 2,
        "name": "Pennsylvania"
      }
    ]
  }
}

Table of Contents:

Setup

First install:

pip install drf-flex-fields

Then have your serializers subclass FlexFieldsModelSerializer:

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.

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:

{
  "id": 13322,
  "name": "John Doe",
  "country": 12,
  "occupation": "Programmer"
}

When you do a GET /person/13322?expand=country, the response will change to:

{
  "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:

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:

{
  "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:

{
  "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:

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.

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:

{
    "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:

{
  "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:

{
  "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:

{
  "id": 13322,
  "name": "John Doe",
  "country": {
    "name": "United States"
  }
}

Or, if you want to leave out the nested country object, do ?omit=country:

{
  "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:

expandable_fields = {
    'record_set': ('<module_path_to_serializer_class>.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: <app_name>.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.

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

serializer = PersonSerializer(
  person,
  expand=["friends.hobbies", "employer"],
  omit="friends.age"
)

Approach #3

class PersonSerializer(FlexFieldsModelSerializer):
  // Your field definitions

  class Meta:
    model = Person
    fields = ["age", "hobbies", "name"]
    expandable_fields = {
      'friends': (
        'serializer.FriendSerializer',
        {'many': True, "expand": ["hobbies"], "omit": ["age"]}
      )
    }
Option Description
expand Fields to expand; must be configured in the serializer's expandable_fields
fields Fields that should be included; all others will be excluded
omit Fields that should be excluded; all others will be included

Advanced

Customization

Parameter names and wildcard values can be configured within a Django setting, named REST_FLEX_FIELDS.

Option Description Default
EXPAND_PARAM The name of the parameter with the fields to be expanded "expand"
MAXIMUM_EXPANSION_DEPTH The max allowed expansion depth. By default it's unlimited. Expanding state.towns would equal a depth of 2 None
FIELDS_PARAM The name of the parameter with the fields to be included (others will be omitted) "fields"
OMIT_PARAM The name of the parameter with the fields to be omitted "omit"
RECURSIVE_EXPANSION_PERMITTED If False, an exception is raised when a recursive pattern is found True
WILDCARD_VALUES List of values that stand in for all field names. Can be used with the fields and expand parameters.

When used with expand, a wildcard value will trigger the expansion of all expandable_fields at a given level.

When used with fields, all fields are included at a given level. For example, you could pass fields=name,state.* if you have a city resource with a nested state in order to expand only the city's name field and all of the state's fields.

To disable use of wildcards, set this setting to None.
["*", "~all"]

For example, if you want your API to work a bit more like JSON API, you could do:

REST_FLEX_FIELDS = {"EXPAND_PARAM": "include"}

Defining Expansion and Recursive Limits on Serializer Classes

A maximum_expansion_depth integer property can be set on a serializer class.

recursive_expansion_permitted boolean property can be set on a serializer class.

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

Serializer Introspection

When using an instance of FlexFieldsModelSerializer, you can examine the property expanded_fields to discover which fields, if any, have been dynamically expanded.

Use of Wildcard to Match All Fields

You can pass expand=* (or another value of your choosing) 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:

{
  "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:

{
  "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:

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': (
        'rest_flex_fields.filter_backends.FlexFieldsFilterBackend',
        # ...
    ),
    # ...
}

It will automatically call select_related and prefetch_related on the current QuerySet by determining which fields are needed from many-to-many and foreign key-related models. For sparse fields requests (?omit=fieldX,fieldY or ?fields=fieldX,fieldY), the backend will automatically call only(*field_names) using only the fields needed for serialization.

WARNING: The optimization currently works only for one nesting level.

Changelog

1.0.2 (March 2023)

  • Adds control over whether recursive expansions are allowed and allows setting the max expansion depth. Thanks @andruten!

1.0.1 (March 2023)

  • Various bug fixes. Thanks @michaelschem, @andruten, and @erielias!

1.0.0 (August 2022)

  • Improvements to the filter backends for generic foreign key handling and docs generation. Thanks @KrYpTeD974 and @michaelschem!

0.9.9 (July 2022)

  • Fixes bug in FlexFieldsFilterBackend. Thanks @michaelschem!
  • Adds FlexFieldsDocsFilterBackend for schema population. Thanks @Rjevski!

0.9.8 (April 2022)

  • Set expandable fields as the default example for expand query parameters in coreapi.Field. Thanks @JasperSui!

0.9.7 (January 2022)

  • Includes m2m in prefetch_related clause even if they're not expanded. Thanks @pablolmedorado and @ADR-007!

0.9.6 (November 2021)

  • Make it possible to use wildcard values with sparse fields requests.

0.9.5 (October 2021)

  • Adds OpenAPI support. Thanks @soroush-tabesh!
  • Updates tests for Django 3.2 and fixes deprecation warning. Thanks @giovannicimolin!

0.9.3 (August 2021)

  • Fixes bug where custom parameter names were not passed when constructing nested serializers. Thanks @Kandeel4411!

0.9.2 (June 2021)

  • Ensures context dict is passed down to expanded serializers. Thanks @nikeshyad!

0.9.1 (June 2021)

  • No longer auto removes source argument if it's equal to the field name.

0.9.0 (April 2021)

  • Allows fully qualified import strings for lazy serializer classes.

0.8.9 (February 2021)

  • Adds OpenAPI support to experimental filter backend. Thanks @LukasBerka!

0.8.8 (September 2020)

  • Django 3.1.1 fix. Thansks @NiyazNz!
  • Docs typo fix. Thanks @zakjholt!

0.8.6 (September 2020)

  • Adds is_included utility function.

0.8.5 (May 2020)

  • Adds options to customize parameter names and wildcard values. Closes #10.

0.8.1 (May 2020)

  • Fixes #44, related to the experimental filter backend. Thanks @jsatt!

0.8.0 (April 2020)

  • Adds support for expand, omit and fields query parameters for non-GET requests.
    • The common use case is creating/updating a model instance and returning a serialized response with expanded fields
    • Thanks @kotepillar for raising the issue (#25) and @Crocmagnon for the idea of delaying field modification to to_representation().

0.7.5 (February 2020)

  • Simplifies declaration of expandable_fields
    • If using a tuple, the second element - to define the serializer settings - is now optional.
    • Instead of a tuple, you can now just use the serializer class or a string to lazily reference that class.
    • Updates documentation.

0.7.0 (February 2020)

  • Adds support for different ways of passing arrays in query strings. Thanks @sentyaev!
  • Fixes attribute error when map is supplied to split levels utility function. Thanks @hemache!

0.6.1 (September 2019)

  • Adds experimental support for automatically SQL query optimization via a FlexFieldsFilterBackend. Thanks ADR-007!
  • Adds CircleCI config file. Thanks mikeIFTS!
  • Moves declaration of expandable_fields to Meta class on serialzer for consistency with DRF (will continue to support declaration as class property)
  • Python 2 is no longer supported. If you need Python 2 support, you can continue to use older versions of this package.

0.5.0 (April 2019)

  • Added support for omit keyword for field exclusion. Code clean up and improved test coverage.

0.3.4 (May 2018)

  • Handle case where request is None when accessing request object from serializer. Thanks @jsatt!

0.3.3 (April 2018)

  • Exposes FlexFieldsSerializerMixin in addition to FlexFieldsModelSerializer. Thanks @jsatt!

Testing

Tests are found in a simplified DRF project in the /tests folder. Install the project requirements and do ./manage.py test to run them.

License

See License.

drf-flex-fields's People

Contributors

adr-007 avatar allanlewis avatar andruten avatar crocmagnon avatar dependabot[bot] avatar erielias avatar fladi avatar fvgoto avatar giovannicimolin avatar hemache avatar jaspersui avatar jsatt avatar lukasberka avatar michaelschem avatar mikeifts avatar nikeshyad avatar niyaznz avatar oliwarner avatar pablolmedorado avatar rjevski avatar rsinger86 avatar sentyaev avatar soerenbe avatar soroush-tabesh avatar tanoabeleyra avatar xjlin0 avatar zakjholt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

drf-flex-fields's Issues

Support for expandable `SerializerMethodFields`

Hello, I have a few large SerializerMethodFields on my serializer, which return iterables that are not a FK to my model. Something like this:

Class FooSerializer(FlexFieldsModelSerializer):
    things = SerializerMethodField()

    def get_things(self, instance):
        return ThingSerializer(get_things_related_to_this_instance(instance), many=True).data

As far as I know I cannot directly leverage drf-flex-fields to make this field expandable, because expandable_fields are a statically defined dict that takes a serializer class or tuple.

I have hacked around this by making these fields omitted by default, unless declared in the expand argument:

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)

    expandable_method_fields = ('things', 'more_things')

    if '~all' in self._flex_options['expand'] or '*' in self._flex_options['expand']:
        return

    for field in expandable_method_fields:
        if (
            field not in self._flex_options['expand']
            and field not in self._flex_options['omit']
        ):
            self._flex_options['omit'].append(field)

It works well enough for my purposes, but I thought I'd pitch this as a feature request, in case it is helpful or others have solved the problem differently. Cheers, and thanks for the library!

Expand not working as expected

I have the following models:

class UploadTemplate(models.Model):
    book = models.ForeignKey('Book', on_delete=models.SET_NULL, null=True)
    coupon_type = models.CharField(max_length=100)
class Book(models.Model):
    book_name = models.CharField(max_length=30)
    #....random other fields

and serializers as follows:

class TemplateSerializer(FlexFieldsModelSerializer):
    book = serializers.PrimaryKeyRelatedField(read_only=True)
    class Meta:
        model = UploadTemplate
        fields = ["coupon_type", "id", "book"]
        expandable_fields = {
            'book': ('BookSerializer', {
                'source': 'book'
            })
        }
class BookSerializer(FlexFieldsModelSerializer):
    class Meta:
        model = Book
        fields = "__all__"

If i make a call to http://localhost:8000/v1/coupons/templates/6/?expand=book I'd expect book to expand to the nested serializer. Unfortunately it doesn't and I've drawn a blank debugging.

My viewset:

from rest_flex_fields.views import FlexFieldsMixin

class UploadTemplateViewset(viewsets.ModelViewSet, FlexFieldsMixin):
    queryset = models.UploadTemplate.objects.all()
    serializer_class = serializers.UploadTemplateSerializer
    filterset_fields = ('book', )
    permit_list_expands = ['book']

    def get_queryset(self):
        print(is_expanded(self.request, 'book'))
        return models.UploadTemplate.objects.all()

confirms that book is expanded. (i,e, it prints True in the console).

Am I doing something obviously dumb, and if not, where should I start with debugging the issue? I'm on django 2, python 3.6

OpenAPI support for query parameters

Would it be possible to have OpenAPI support for query parameters?

At the moment this is provided by the FlexFieldsFilterBackend however the filter backend also implements the (not thoroughly tested) query optimization features, which may not be desirable.

An easy fix would be to just separate the docs into its own (dummy) filter backend that doesn't actually do anything query-wise, so people who only want docs can include that one. It does feel a bit hacky to make an essentially fake filtering backend just for documentation so I'd like to get others' thoughts on this if there's a better way.

Expand feature in multi database

HI,
I have to use drf-flex-field in multi database architecture.
Currently I am looping through all the databases and fetching the result. but when I pass expand it breaks and raise error that Model doesn't exists. because it is selecting default database.

Is there any way to pass the database name to be using when expanding and serialising?

dot-notation not working

Based on https://github.com/rsinger86/drf-flex-fields#dynamically-setting-fields,

Here are my serializers:

class AddressBookSerializer(FlexFieldsModelSerializer):
    class Meta:
        model = SellerAddressBook
        fields = ('id', 'address', 'city', 'state', )
class OrderSerializer(FlexFieldsModelSerializer):
    address = AddressBookSerializer(many=False)

    class Meta:
        model = Order
        fields = ('id', 'order_name', 'address',)

On Get, /orders/123/?fields=id,order_name,address.city

ACTUAL RESULT

{
  "id" : 123
  "order_name" : "Order Name",
  "address" : {
    "id" : "1",
    "address": "my add",
    "city": "my_city",
    "state": " my state"
}

EXPECTED RESULT

{
  "id" : 13322
  "order_name" : "Order Name",
  "address" : {
      "city": "my_city"
 }

FlexFieldsFilterBackend does not handle "GenericForeignKey"

When calling a ViewSet that has both the FlexFieldsFilterBackend and a serializer containing a field pointing to a GenericForeignKey, we get the error : AttributeError: 'GenericForeignKey' object has no attribute 'attname'

It can easily be reproduced with the following classes :
models

class TaggedItem(models.Model):
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    link = ForeignKey(SomeTaggableModel, null=True, on_delete=models.CASCADE)

serializers

class TaggedItemSerializer(FlexFieldsSerializerMixin, ModelSerializer):
    content_object = PrimaryKeyRelatedField(read_only=True) 
    class Meta:
        model = TaggedItem
        fields = (
            "id",
            "content_type",
            "object_id",
            "content_object"
        )

Please notice that the PrimaryKeyRelatedField is only here for simplicity, in real life it could be a GenericRelatedField from
rest-framework-generic-relations.

views

class TaggedItemViewSet(ModelViewSet):
    serializer_class = TaggedItemSerializer
    queryset = TaggedItem.objects.all()
    filter_backends = [FlexFieldsFilterBackend]

After digging a bit, I found that the field gets included in the queryset.only(...) and crashes probably because it is not a concrete field. A GenericForeignKey should, instead, be included in the queryset.prefetch_related(...) fields.

`~all` or `*` should work in `fields` too

Hey there! I was looking at graphql until I found you awesome library, thank you! I don't have to add another yet API ๐Ÿ—ก๏ธ

The question is:
We have two models with a lot of fields, e.g

class Order:
    field0 = ....
    field1 = ...
    ...
    field35 = ...
   restaurant=Restaurant

class Restaurant:
   field0 = ...
   field1 = ...
   ...
   field60 = ....

And I want to make a request that will get All fields from Order, expand Restaurant, but only field0\field1
How can I achieve it with drf-flex?

I tried to solve it these ways:

  1. GET /orders/?expand=restaurant - but it gives too much fields, I really want to only two from Restaurant because we have about 100 orders in a list and all of them will contain 60 additional fields
  2. GET /orders/?fields=field0,field1,...,field35,restaurant.field0,restaurant.field1&expand=restaurant - In this case I get that I want to, but I have to add all of fields to params, and I really don't want to do it on the frontend :)
  3. GET /orders/?expand=restaurant - give me too much fields too

What I really want to can be achieved with GET /orders/?fields=~all,restaurant.field0, restaurant.field2&expand=restaurant

But looks like ~all works only in expand and not on fields, right?

Omitted fields still appears to allow saving omitted field on the model

When passing omitted fields to the serializer in a views' get_serializer method, it still seems to be possible to save/update said fields, even though they're not returned in the Response.

For example:

def get_serializer(self, *args, **kwargs):
    serializer_class = self.get_serializer_class()
    kwargs.setdefault('context', self.get_serializer_context())
    kwargs.update({'omit': 'example_field'})
    return serializer_class(*args, **kwargs)

Will not return example_field in the response, however any value passed into a PUT or PATCH request for that field will still be saved on the model.

Fields don't seem to be omitted until to_representation is called, but the lateness of that call means that the serializer still contains these fields up until the response is prepared. Is this the expected behavior? It seems contrary to what I would expect.

Django 3.2 support

I've run the test suite for this package on Django 3.2, and it only required minor changes to work (and still be Django 3.1 compatible).

Would you accept a PR to upgrade this package's support to Django 3.2 and fix some deprecation warnings? Let me know if so.

URL querystring not parsed correctly in 0.7.0

It seems that since upgrading to 0.7.0, drf-flex-fields no longer parses expansions requested via URL querystring the same way.

I have been doing this:

class EphemeronViewSet(FlexFieldsModelViewSet):
    [...]
    def get_serializer(self, *args, **kwargs):
        if "expand" in self.request.query_params:  # add from querystring in URL
            kwargs["expand"] = self.request.query_params['expand']
        return super(EphemeronViewSet, self).get_serializer(*args, **kwargs)

which allows expanding fields like this:

curl '0.0.0.0:8000/api/ephemera/?expand=user'

But since upgrading to 0.7.0, the field no longer gets expanded in the output.

I think these changed lines are what resulted in the change in behavior. If I add a print(passed) just after that and compare previous behavior to 0.7.0:

Before:

{'expand': 'user', 'fields': [], 'omit': []}

After:

{'expand': ['u', 's', 'e', 'r'], 'fields': [], 'omit': []}

Perhaps these lines should be something more like [kwargs.pop("expand")] if "expand" in kwargs else [] instead of list(kwargs.pop("fields", []))?

(Or perhaps there's a better way to pass querystrings to drf-flex-fields than what I've been doing in my get_serializer() override as shown above? I have been doing it that way in order to expand fields in response to a POST request, if I recall recorrectly.)

`is_expanded` logic for nested fields is incorrect

  1. If I have nested fields such that a.b and b are both legal expands, is_epxanded will not distinguish between b at the top level or nested
  2. is_expanded will return true for all keys if ~all is in expanded, but ~all only expands top level expands

Proposed fix:

def is_expanded(expand, key):
    """Determine if the given key is expanded"""
    expand_fields = []

    # first split on commas to get each expand
    for full in expand.split(","):
        # than split on dots to get each component that is expanded
        parts = full.split(".")
        for i in range(len(parts)):
            # add each prefix, as each prefix is epxanded, ie
            # a.b.c will add a, a.b and a.b.c to the expand_fields list
            # we do this to differentiate a.b from b
            expand_fields.append(".".join(parts[: i + 1]))

    # ~all only expands top level fields
    if "." not in key and "~all" in expand_fields:
        return True

    return key in expand_fields

Test:

import pytest

from .utils import is_expanded

data = [
    ("a", "a", True),
    ("a", "b", False),
    ("a,b,c", "a", True),
    ("a,b,c", "b", True),
    ("a,b,c", "c", True),
    ("a,b,c", "d", False),
    ("a.b.c", "a", True),
    ("a.b.c", "a.b", True),
    ("a.b.c", "a.b.c", True),
    ("a.b.c", "b", False),
    ("a.b.c", "c", False),
    ("a.b.c", "d", False),
    ("a.b.c,d", "a", True),
    ("a.b.c,d", "d", True),
    ("~all", "a", True),
    ("~all", "a.b", False),
]


class TestIsExpanded:
    @pytest.mark.parametrize("expand,key,ret", data)
    def test_expanded(self, expand, key, ret):
        assert is_expanded(expand, key) is ret

How to pass context to child serializer?

I'm facing a RecursionError when querying a subset of fields that should not make recursion at all.

Here are some simplified models and serializers:

# disclaimer: I did not directly test this code, it's just an extract of mine.
# If you don't manage to reproduce the issue with this snippet, please let me know

class Client(models.Model):
    name = models.CharField(max_length=250)

class Project(models.Model):
    name = models.CharField(max_length=250)
    client = models.ForeignKey(Client, on_delete=models.PROTECT, related_name='projects')

class ProjectSerializer(FlexFieldsModelSerializer):
    expandable_fields = {
        'client_details': ('api.ClientSerializer', {'source': 'client', 'read_only': True}),
    }
    class Meta:
        model = Project
        fields = [
            'id',
            'name',
        ]

class ClientSerializer(FlexFieldsModelSerializer):
    expandable_fields = {
        'projects_details': (ProjectSerializer, {'source': 'projects', 'many': True, 'read_only': True}),
    }
    class Meta:
        model = Client
        fields = [
            'id',
            'name',
        ]

I queried my endpoint like this:

/api/client/5906?expand=projects_details&fields=id,name,projects_details.id

The expected result would be:

{
  "id": 5906,
  "name": "client name",
  "projects_details": [
    {
      "id": 2056
    },
    {
      "id": 3323
    }
  ]
}

Instead, I'm getting a RecursionError (see below). Did I miss something ? I understand that since I'm requesting to expand the projects and the projects themselves have a reference to the clients, but given the fields input, I feel like this should not fall in recursion.

RecursionError at /api/client/5906
maximum recursion depth exceeded

Request Method: GET
Request URL: http://localhost:81/api/client/5906?expand=projects_details&fields=id,name,projects_details.id
Django Version: 2.1.9
Python Executable: C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\Scripts\python.exe
Python Version: 3.7.3
Python Path: ****
Server time: Mon, 24 Jun 2019 17:47:09 +0200
Installed Applications:
['django.contrib.admindocs',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'rest_framework',
 'django_filters',
 'corsheaders',
 'api',
 'custom_auth']
Installed Middleware:
['corsheaders.middleware.CorsMiddleware',
 'django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.auth.middleware.RemoteUserMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']


Traceback:

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\django\core\handlers\exception.py" in inner
  34.             response = get_response(request)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\django\core\handlers\base.py" in _get_response
  126.                 response = self.process_exception_by_middleware(e, request)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\django\core\handlers\base.py" in _get_response
  124.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\django\views\decorators\csrf.py" in wrapped_view
  54.         return view_func(*args, **kwargs)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\django\views\generic\base.py" in view
  68.             return self.dispatch(request, *args, **kwargs)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_framework\views.py" in dispatch
  495.             response = self.handle_exception(exc)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_framework\views.py" in handle_exception
  455.             self.raise_uncaught_exception(exc)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_framework\views.py" in dispatch
  492.             response = handler(request, *args, **kwargs)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_framework\generics.py" in get
  284.         return self.retrieve(request, *args, **kwargs)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_framework\mixins.py" in retrieve
  57.         serializer = self.get_serializer(instance)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_framework\generics.py" in get_serializer
  112.         return serializer_class(*args, **kwargs)

File ".\api\serializers.py" in __init__
  230.         super().__init__(*args, **kwargs)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_flex_fields\serializers.py" in __init__
  47.                 name, next_expand_fields, next_sparse_fields, next_omit_fields

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_flex_fields\serializers.py" in _make_expanded_field_serializer
  58.         serializer_settings = copy.deepcopy(field_options[1])

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\copy.py" in deepcopy
  150.         y = copier(x, memo)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\copy.py" in _deepcopy_dict
  240.         y[deepcopy(key, memo)] = deepcopy(value, memo)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\copy.py" in deepcopy
  150.         y = copier(x, memo)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\copy.py" in _deepcopy_dict
  240.         y[deepcopy(key, memo)] = deepcopy(value, memo)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\copy.py" in deepcopy
  180.                     y = _reconstruct(x, memo, *rv)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\copy.py" in _reconstruct
  281.         if hasattr(y, '__setstate__'):

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_framework\request.py" in __getattr__
  412.             return getattr(self._request, attr)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_framework\request.py" in __getattr__
  412.             return getattr(self._request, attr)

File "C:\Users\augendre\.virtualenvs\bizdev_kb_api-TgTXDkAC\lib\site-packages\rest_framework\request.py" in __getattr__
  412.             return getattr(self._request, attr)

[...]

Exception Type: RecursionError at /api/client/5906
Exception Value: maximum recursion depth exceeded
Request information:
USER: ****

GET:
expand = 'projects_details'
fields = 'id,name,projects_details.id'

OpenAPI support for flex fields

I am using drf-spectacular for schema generation; using the @extend_schema decorator to add docs to my views.

Say I have a serializer named PersonSerializer that inherits from FlexFieldsModelSerializer. If I pass it the fields argument, I expect it to show only those fields in the schema. Example code:

@extend_schema(
    responses={201: PersonSerializer(fields=["id", "username"])},
)
@api_view(["POST"])
def my_view(request):

But the schema picks up all the default fields from the PersonSerializer class.

I do not know if this is something that needs to be solved in this project or drf-spectacular but any help is greatly appreciated.

AppRegistryNotReady caused by __init__.py

The easy fix is to not import everything into __init__, but that changes the API. The bad solution is to tell people to not list the app in INSTALLED_APPS. The ugly solution is to change views.py to import DRF internals inside the methods where they are needed, not importing at the top.

python3 manage.py migrate

  ...
  File "/usr/lib/python3.8/site-packages/rest_flex_fields/__init__.py", line 3, in <module>
    from .views import FlexFieldsModelViewSet
  File "/usr/lib/python3.8/site-packages/rest_flex_fields/views.py", line 6, in <module>
    from rest_framework import viewsets
  File "/usr/lib/python3.8/site-packages/rest_framework/viewsets.py", line 27, in <module>
    from rest_framework import generics, mixins, views
  File "/usr/lib/python3.8/site-packages/rest_framework/generics.py", line 9, in <module>
    from rest_framework import mixins, views
  File "/usr/lib/python3.8/site-packages/rest_framework/views.py", line 17, in <module>
    from rest_framework.schemas import DefaultSchema
  File "/usr/lib/python3.8/site-packages/rest_framework/schemas/__init__.py", line 33, in <module>
    authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
  File "/usr/lib/python3.8/site-packages/rest_framework/settings.py", line 220, in __getattr__
    val = perform_import(val, attr)
  File "/usr/lib/python3.8/site-packages/rest_framework/settings.py", line 168, in perform_import
    return [import_from_string(item, setting_name) for item in val]
  File "/usr/lib/python3.8/site-packages/rest_framework/settings.py", line 168, in <listcomp>
    return [import_from_string(item, setting_name) for item in val]
  File "/usr/lib/python3.8/site-packages/rest_framework/settings.py", line 177, in import_from_string
    return import_string(val)
  File "/usr/lib/python3.8/site-packages/django/utils/module_loading.py", line 17, in import_string
    module = import_module(module_path)
  File "/usr/lib64/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/usr/lib/python3.8/site-packages/rest_framework_jwt/authentication.py", line 17, in <module>
    from rest_framework_jwt.blacklist.exceptions import (
  File "/usr/lib/python3.8/site-packages/rest_framework_jwt/blacklist/exceptions.py", line 8, in <module>
    class MissingToken(AuthenticationFailed):
  File "/usr/lib/python3.8/site-packages/rest_framework_jwt/blacklist/exceptions.py", line 10, in MissingToken
    msg = _('The token is missing.')
  File "/usr/lib/python3.8/site-packages/django/utils/translation/__init__.py", line 105, in ugettext
    return gettext(message)
  File "/usr/lib/python3.8/site-packages/django/utils/translation/__init__.py", line 92, in gettext
    return _trans.gettext(message)
  File "/usr/lib/python3.8/site-packages/django/utils/translation/trans_real.py", line 354, in gettext
    _default = _default or translation(settings.LANGUAGE_CODE)
  File "/usr/lib/python3.8/site-packages/django/utils/translation/trans_real.py", line 267, in translation
    _translations[language] = DjangoTranslation(language)
  File "/usr/lib/python3.8/site-packages/django/utils/translation/trans_real.py", line 154, in __init__
    self._add_installed_apps_translations()
  File "/usr/lib/python3.8/site-packages/django/utils/translation/trans_real.py", line 195, in _add_installed_apps_translations
    raise AppRegistryNotReady(
django.core.exceptions.AppRegistryNotReady: The translation infrastructure cannot be initialized before the apps registry is ready. Check that you don't make non-lazy gettext calls at import time.

Also occurs with drf_extended_viewset and beda-software/drf-writable-nested#93

Missing tag for 0.9.4

v0.9.4 is published on PyPi but there isn't a corresponding tag (or source) in this repo.

Create unique type names in FlexFieldsMixin

All fields that are expandable will get the same serializer names. It's not directly an issue with this library but this causes issues with (for example) schema generation where names -can- be based on serializer class names.

Perhaps include the original serializer name in the generated type name.

In views.py:50 change this:

-		return type('DynamicFieldsModelSerializer', (self.serializer_class,), {
+		return type('{}FlexFieldsSerializer'.format(self.serializer_class.__name__), (self.serializer_class,), {
			'expand': expand, 
			'include_fields': fields,
		})

Deferred fields?

Documentation talks about deferring fields but I can't get that to work. Just so we're on the same page, I have a couple of statistic fields that I want to make available but I don't want included in every query. These are annotated and pretty expensive (hence the deferral).

So here, I want to be able to annotate and display the last visit (the end of the last booking a Visitor had). The annotation itself โ€”despite looking pretty gnarlyโ€” works.... But throwing expand=last_visit or expand=last_visit&fields=last_visit through on the querystring doesn't make the serialiser render this field.

class VisitorSerializer(FlexFieldsModelSerializer):
    class Meta:
        model = Visitor
        fields = ('first_name', 'second_name')
        expandable_fields = {
            'last_visit': (serializers.DateTimeField, {})
        }

class VisitorViewSet(viewsets.ModelViewSet):
    permission_classes = (IsAuthenticated,)
    queryset = Visitor.objects.all()
    serializer_class = VisitorSerializer

    permit_list_expands = ['last_visit']

    def get_queryset(self):
        queryset = super().get_queryset()

        if is_expanded(self.request, 'last_visit'):
            subq = Booking.objects.filter(visitor_bookings__visitor=OuterRef('pk')).values('timeframe')
            queryset = queryset.annotate(
                last_visit=RangeEndsWith(Subquery(subq))
            )

        return queryset

Reverse Relationships?

Hello,

I may be missing something, so I apologize if I am, but I am unable to get DRF-Flex-Fields to work properly with a reverse relationships . Is this not something that drf-flex-fields can do or am I messing up somehow?

Better notation for deferred fields?

Hello and hope you're well!

I wanted to raise a discussion on how deferred fields are currently defined and whether a less verbose approach could be supported?

At the moment my understanding is that expandable fields explicitly need to have their serializer (or field type) defined. This is fine for "true" expands (that warrant a separate serializer) but becomes unnecessarily verbose for fields on the same model - those only defined in fields and the ModelSerializer infers the actual field types at runtime from the model.

Given this serializer:

class MySerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = ("id", "name", "description", "etc")

Let's say I wanted to have description and etc deferred - not rendered by default unless requested, currently I'd have to do this:

class MySerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = ("id", "name")
        expandable_fields = {"description" serializers.CharField, "etc": serializers.CharField}

This requires explicitly listing out field classes for every field, a pretty tedious process.

In the codebase I'm currently working on we worked around this as follows:

class CustomFlexFieldsSerializerMixin(FlexFieldsSerializerMixin):
    """
    Overriding the FlexFieldsSerializerMixin to enable declaring of "default_fields"
    in the Serializer.Meta.
    This is a list of fields to be shown if no "fields" parameter is present.

    class Meta:
        default_fields = ["id", "name"]
    """

    def __init__(self, *args, **kwargs):
        """Set fields from Meta.default_fields if not provided in the parameters"""
        if (
            kwargs.get("context")
            and not kwargs["context"]["request"].query_params.getlist(FIELDS_PARAM)
            and not kwargs["context"]["request"].query_params.getlist(OMIT_PARAM)
        ):
            super().__init__(*args, **kwargs, fields=self._default_fields)
        else:
            super().__init__(*args, **kwargs)

    @property
    def _default_fields(self) -> dict:
        if hasattr(self, "Meta") and hasattr(self.Meta, "default_fields"):
            return self.Meta.default_fields
        return {}

Essentially the above approach sets the fields argument to Meta.default_fields (unless it's explicitly set within the context from the originating request) as if they were explicitly requested via the query string - this allows you to have deferrable fields with minimal changes to the serializer - just set default_fields and you're good to go.

We had a TODO in there to upstream this so I wanted to raise this discussion to see if there's a way we can merge our approaches so our custom override above is no longer required.

AttributeError 'map' object has no attribute 'split'

Hello,

I started getting this error after upgrading to Python 3 where fields is a generator and not a list.

Traceback:  

File "/app/.venv/lib/python3.7/site-packages/django/core/handlers/exception.py" in inner
  41.             response = get_response(request)

File "/app/.venv/lib/python3.7/site-packages/django/core/handlers/base.py" in _legacy_get_response
  249.             response = self._get_response(request)

File "/app/.venv/lib/python3.7/site-packages/django/core/handlers/base.py" in _get_response
  187.                 response = self.process_exception_by_middleware(e, request)

File "/app/.venv/lib/python3.7/site-packages/django/core/handlers/base.py" in _get_response
  185.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/app/.venv/lib/python3.7/site-packages/django/views/decorators/csrf.py" in wrapped_view
  58.         return view_func(*args, **kwargs)

File "/app/.venv/lib/python3.7/site-packages/rest_framework/viewsets.py" in view
  103.             return self.dispatch(request, *args, **kwargs)

File "/app/.venv/lib/python3.7/site-packages/rest_framework/views.py" in dispatch
  483.             response = self.handle_exception(exc)

File "/app/.venv/lib/python3.7/site-packages/rest_framework/views.py" in handle_exception
  443.             self.raise_uncaught_exception(exc)

File "/app/.venv/lib/python3.7/site-packages/rest_framework/views.py" in dispatch
  480.             response = handler(request, *args, **kwargs)

File "/app/.venv/lib/python3.7/site-packages/rest_framework/mixins.py" in list
  44.             serializer = self.get_serializer(page, many=True)

File "/app/.venv/lib/python3.7/site-packages/rest_framework/generics.py" in get_serializer
  112.         return serializer_class(*args, **kwargs)

File "/app/.venv/lib/python3.7/site-packages/rest_framework/serializers.py" in __new__
  124.             return cls.many_init(*args, **kwargs)

File "/app/.venv/lib/python3.7/site-packages/rest_framework/serializers.py" in many_init
  145.         child_serializer = cls(*args, **kwargs)

File "/app/.venv/lib/python3.7/site-packages/rest_flex_fields/serializers.py" in __init__
  34.         sparse_fields, next_sparse_fields = split_levels(fields)

File "/app/.venv/lib/python3.7/site-packages/rest_flex_fields/utils.py" in split_levels
  27.         fields = [a.strip() for a in fields.split(",") if a.strip()]

AttributeError at /api/v2/users/
'map' object has no attribute 'split'

Running on Python 3.7

Django==1.11.25
djangorestframework==3.8.2
drf-flex-fields==0.6.1

I can submit a PR to fix this issue if you don't mind

Adding FlexFieldsFilterBackend immediately breaks the app

I am utilizing drf-flex-fields heavily and everything works fine until the moment i add:

REST_FRAMEWORK = {
    ...
    'DEFAULT_FILTER_BACKENDS': (
        'rest_flex_fields.filter_backends.FlexFieldsFilterBackend',
    ),
    ...
}

It crashes the app with the following traceback:

INFO 2020-03-03 21:38:35,864 autoreload 90410 4456287680 Watching for file changes with StatReloader
Performing system checks...

Exception in thread django-main-thread:
Traceback (most recent call last):
  File "/usr/local/Cellar/[email protected]/3.8.1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py", line 932, in _bootstrap_inner
    self.run()
  File "/usr/local/Cellar/[email protected]/3.8.1/Frameworks/Python.framework/Versions/3.8/lib/python3.8/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/utils/autoreload.py", line 53, in wrapper
    fn(*args, **kwargs)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/core/management/commands/runserver.py", line 117, in inner_run
    self.check(display_num_errors=True)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/core/management/base.py", line 392, in check
    all_issues = self._run_checks(
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/core/management/base.py", line 382, in _run_checks
    return checks.run_checks(**kwargs)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/core/checks/registry.py", line 72, in run_checks
    new_errors = check(app_configs=app_configs)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/core/checks/urls.py", line 13, in check_url_config
    return check_resolver(resolver)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/core/checks/urls.py", line 23, in check_resolver
    return check_method()
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/urls/resolvers.py", line 407, in check
    for pattern in self.url_patterns:
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/urls/resolvers.py", line 588, in url_patterns
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/urls/resolvers.py", line 581, in urlconf_module
    return import_module(self.urlconf_name)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/alex.zagoro/projects/gagosian/noya/gagosian/urls.py", line 6, in <module>
    path('api/v1/', include('gagosian.api.v1.urls', namespace='api'))
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/urls/conf.py", line 34, in include
    urlconf_module = import_module(urlconf_module)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/alex.zagoro/projects/gagosian/noya/gagosian/api/v1/urls.py", line 4, in <module>
    from .contacts import urls as contacts_urls
  File "/Users/alex.zagoro/projects/gagosian/noya/gagosian/api/v1/contacts/urls.py", line 3, in <module>
    from .viewsets import (
  File "/Users/alex.zagoro/projects/gagosian/noya/gagosian/api/v1/contacts/viewsets.py", line 1, in <module>
    from rest_framework.viewsets import ModelViewSet
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/rest_framework/viewsets.py", line 27, in <module>
    from rest_framework import generics, mixins, views
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/rest_framework/generics.py", line 24, in <module>
    class GenericAPIView(views.APIView):
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/rest_framework/generics.py", line 43, in GenericAPIView
    filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/rest_framework/settings.py", line 220, in __getattr__
    val = perform_import(val, attr)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/rest_framework/settings.py", line 168, in perform_import
    return [import_from_string(item, setting_name) for item in val]
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/rest_framework/settings.py", line 168, in <listcomp>
    return [import_from_string(item, setting_name) for item in val]
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/rest_framework/settings.py", line 177, in import_from_string
    return import_string(val)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/django/utils/module_loading.py", line 17, in import_string
    module = import_module(module_path)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/rest_flex_fields/__init__.py", line 3, in <module>
    from .views import FlexFieldsModelViewSet
  File "/Users/alex.zagoro/venv/noya-TS4FQGPC/lib/python3.8/site-packages/rest_flex_fields/views.py", line 21, in <module>
    class FlexFieldsModelViewSet(FlexFieldsMixin, viewsets.ModelViewSet):
AttributeError: partially initialized module 'rest_framework.viewsets' has no attribute 'ModelViewSet' (most likely due to a circular import)

Adding the backend directly to the view works fine.

Expandable Fields through SOA architecture

Hi, I created an expansion from your flex fields to allow the usage between APIs.

It is really usefull in a distributed system, where we usually have many weak foreign keys that points to a resource in a different service.

The usage remained the same, so its transparent to the API consumer. It even allows the nested expand/fields/omit through APIs

Do you have interest in a PR with this feature?

Ex:

    class Meta:
        model = FileImportInput
        fields = "__all__"
        expandable_fields = {
            "data_input": (
                APIResourceFlexDict,
                {
                    "url": settings.DATA_INPUT_RESOURCE,
                    "included_headers": ["Authorization"],
                },
            ),
        }

FilterBackend: Include not-expanded M2M fields in prefetch-related clause

Hi Robert;

First of all, thank you so much for such an amazing library! ๐Ÿ˜„

I'm testing the "FlexFieldsFilterBackend" and I think you overlooked something. Please let me know if I'm wrong.

When dealing with M2M fields, it's always desiderable to include them in prefetch_related clause, even if you're working only with pks. Otherwise, django would need to perform an extra query for every single record.

I noticed that you check if the field is expanded before including it in the prefetch_related clause. I think it should be included anyways.

and field.field_name in serializer.expanded_fields

Thanks in advance.

Support arrays in query string for `expan`, `fields` etc

Currently flex-fields support passing lists of fields to expand/omit like this:
?expand=field_1,field_2,field_3

For our internal development there is a request from frontend team to support passing lists like this:

  1. ?expand=field_1,field_2
  2. ?expand=field_1&expand=field_2
  3. ?expand[]=field_1&expand[]=field_2

The reason is - different libs/frameworks has different internal implementation for handling query string arrays, and looks like all this 3 options used.

Currently we do this with overriding _parse_request_list_value method from FlexFieldsModelSerializer.
This is naive and simple implementation:

    def _parse_request_list_value(self, field):
        if not self._can_access_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 []

@rsinger86 Do you interested in adding this to the drf-flex-fields itself? If so, I can create PR then.

Undocumented feature: HyperlinkedRelatedField works as expected

The FlexFieldsModelSerializer returns values for related fields by default:

GET /api/location/region/AKL/

{
    "url": "http://127.0.0.1:8000/api/location/region/AKL/",
    "code": "AKL",
    "name": "Auckland",
    "country": "NZL"
}

But by specifying country as a HyperlinkedRelatedField:

class RegionSerializer(FlexFieldsModelSerializer):
    country = serializers.HyperlinkedRelatedField(
        read_only=True,
        view_name='country-detail'
    )

We get:

{
    "url": "http://127.0.0.1:8000/api/location/region/AKL/",
    "code": "AKL",
    "name": "Auckland",
    "country": "http://127.0.0.1:8000/api/location/country/NZL/"
}

and ?expand=country works as expected. Nice.

Attribute Error when using rest_flex_fields.filter_backends.FlexFieldsFilterBackend

Thanks for this great library, I really love this. I have come across an issue however, when I wanted to use rest_flex_fields.filter_backends.FlexFieldsFilterBackend. When I added it to the configuration of DRF, the following import error occured:

Traceback (most recent call last):
  File "manage.py", line 22, in <module>
    main()
  File "manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "d:\workspace\my_project\api\.env\lib\site-packages\django\core\management\__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "d:\workspace\my_project\api\.env\lib\site-packages\django\core\management\__init__.py", line 377, in execute
    django.setup()
  File "d:\workspace\my_project\api\.env\lib\site-packages\django\__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "d:\workspace\my_project\api\.env\lib\site-packages\django\apps\registry.py", line 114, in populate
    app_config.import_models()
  File "d:\workspace\my_project\api\.env\lib\site-packages\django\apps\config.py", line 211, in import_models
    self.models_module = import_module(models_module_name)
  File "d:\workspace\my_project\api\.env\lib\importlib\__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "d:\workspace\my_project\api\my_app\models.py", line 9, in <module>
    from utils.mixins import ResultsModelMixin, TrackFieldChangesMixin
  File "d:\workspace\my_project\api\utils\mixins\__init__.py", line 4, in <module>
    from .results import ResultsModelMixin, ResultsListViewMixin
  File "d:\workspace\my_project\api\utils\mixins\results\__init__.py", line 2, in <module>
    from .views import ResultsListViewMixin
  File "d:\workspace\my_project\api\utils\mixins\results\views.py", line 5, in <module>
    from rest_framework.generics import get_object_or_404
  File "d:\workspace\my_project\api\.env\lib\site-packages\rest_framework\generics.py", line 24, in <module>
    class GenericAPIView(views.APIView):
  File "d:\workspace\my_project\api\.env\lib\site-packages\rest_framework\generics.py", line 43, in GenericAPIView
    filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
  File "d:\workspace\my_project\api\.env\lib\site-packages\rest_framework\settings.py", line 220, in __getattr__
    val = perform_import(val, attr)
  File "d:\workspace\my_project\api\.env\lib\site-packages\rest_framework\settings.py", line 168, in perform_import
    return [import_from_string(item, setting_name) for item in val]
  File "d:\workspace\my_project\api\.env\lib\site-packages\rest_framework\settings.py", line 168, in <listcomp>
    return [import_from_string(item, setting_name) for item in val]
  File "d:\workspace\my_project\api\.env\lib\site-packages\rest_framework\settings.py", line 177, in import_from_string
    return import_string(val)
  File "d:\workspace\my_project\api\.env\lib\site-packages\django\utils\module_loading.py", line 17, in import_string
    module = import_module(module_path)
  File "d:\workspace\my_project\api\.env\lib\importlib\__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "d:\workspace\my_project\api\.env\lib\site-packages\rest_flex_fields\__init__.py", line 3, in <module>
    from .views import FlexFieldsModelViewSet
  File "d:\workspace\my_project\api\.env\lib\site-packages\rest_flex_fields\views.py", line 6, in <module>
    from rest_framework import viewsets
  File "d:\workspace\my_project\api\.env\lib\site-packages\rest_framework\viewsets.py", line 199, in <module>
    class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
AttributeError: module 'rest_framework.generics' has no attribute 'GenericAPIView'

Removing the imports from .views import FlexFieldsModelViewSet from __init__.py and from rest_framework.viewsets import GenericViewSet from filter_backends.py solved the issue for me. Probably the later could be placed behind the TYPE_CHECKING flag from typing, but I have no idea about the first one.

Env:

Django==3.0.7
django-filter==2.3.0
djangorestframework==3.11.0
drf-flex-fields==0.8.5

Filter Backend for Query optimization using wrong check

In the filter backend for optimizing the queries, the backend is doing a check to confirm that the serializer being used is a valid FlexFields serializer.
https://github.com/rsinger86/drf-flex-fields/blob/master/rest_flex_fields/filter_backends.py#L19

However I think rather than checking for the FlexFieldsModelSerializer, the proper check should be for a instance of FlexFieldsSerailizerMixin, as this mixin is what actually contains the functionality needed by the filter backend.

There are several cases in my project where we are having to build our own ModelSerializers where it doesn't make sense or could break things to introduce an additional ModelSerializer to the MRO, especially just to add the functionality provided by simply adding FlexFieldsSerializerMixin.

Allow omit query_param and omit keyword

I would like to specify some fields to omit when I initialize the serializer and also allow the API request query_param to omit extrfa fields. Right now it's either or but merging the two would be nice. Also the same for fields, possibly.

For example:
This should return all the fields except password &email. Right now, it will only omit password.
FlexSerializer(data=request.data, omit=['password'])
GET /api/?omit=email

      def __init__(self, *args, **kwargs):
        expand = list(kwargs.pop("expand", []))
        fields = list(kwargs.pop("fields", []))
        omit = list(kwargs.pop("omit", []))

        super(FlexFieldsSerializerMixin, self).__init__(*args, **kwargs)

        self.expanded_fields = []
        self._flex_fields_applied = False

        self._flex_options = {
            "expand": (
                expand
                if len(expand) > 0
                else self._get_permitted_expands_from_query_param()
            ),
            "fields": (
                fields if len(fields) > 0 else self._get_query_param_value("fields")
            ),
            "omit": omit if len(omit) > 0 else self._get_query_param_value("omit"),
        }

Can't rename expanded field (because "source" keyword is excluded)

I'm trying to rename a field that is also a nested serializer. Normally, I'd use the source kwarg during serializer instantiation for this, but if I define the serializer in expandable_fields, it doesn't work. In the code below, if I set the field name (in the FooBarSerializer) to foo_bar_details, everything works. But if I set it to details and then include the source field, I get this error (this approach works fine if I instantiate the serializer normally, outside of expandable_fields):

  File "/Users/david/src/proj/venv/lib/python3.7/site-packages/rest_framework/serializers.py", line 1325, in build_unknown_field
    (field_name, model_class.__name__)
django.core.exceptions.ImproperlyConfigured: Field name `details` is not valid for model `FooBar`.
class FooBarDetails(django.db.models.Model):
    id = models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True)
    properties = models.CharField(max_length=256, null=True, blank=True)  
    class Meta:
        db_table = 'foo_bar_details'

class FooBar(django.db.models.Model):
    id = models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True)
    name = models.CharField(max_length=256, null=True, blank=True)  
    foo_bar_details = models.ForeignKey(FooBarDetails, on_delete=models.SET_NULL, null=True)
    class Meta:
        db_table = 'foo_bar'

class FooBarDetailsSerializer(FlexFieldsModelSerializer):
    class Meta:
        model = FooBarDetails
        fields = ('properties')

class FooBarSerializer(FlexFieldsModelSerializer):
    expandable_fields = {
        'details': (
            FooBarDetailsSerializer,
            {'required': False, 'many': False, 'read_only': True, 'source': 'foo_bar_details'},
        ),
    }
    class Meta:
        model = FooBar
        fields = (
            'id',
            'name',
            'details',
        )

I think I've traced this issue to here: https://github.com/rsinger86/drf-flex-fields/blob/master/rest_flex_fields/serializers.py#L102
Is there a reason that the source keyword is purposefully being excluded from the serializer instantiation?

Doesn't work out of the gate

Not sure what I'm doing wrong, but I'd really love to get this fixed after following the README pretty exact I think.

serializers.py

from rest_flex_fields import FlexFieldsModelSerializer

class CastleSerializer(FlexFieldsModelSerializer):
    class Meta:
        model = Castle
        fields = '__all__'

class RoomSerializer(FlexFieldsModelSerializer):
    castle = serializers.PrimaryKeyRelatedField(read_only=True)

    class Meta:
        model = Room
        fields = ('id', 'name', 'castle')

        expandable_fields = {
            "castle": CastleSerializer
        }

settings.py

# To make our nested serializers more like JSON API
REST_FLEX_FIELDS = {"EXPAND_PARAM": "include"}

API call: GET /api/v1/room/1/?include=castle
or
API call: GET /api/v1/rooms/?include=castle

both give:

{
    "id": 1,
    "name": "Throne Room",
    "castle": 1
}

OR

[
  {
    "id": 1,
    "name": "Throne Room",
    "castle": 1
  }
]

I expect it to be:

{
    "id": 1,
    "name": "Throne Room",
    "castle": {
        "id": 1,
        "name": "Daventry Castle"
    }
}

I also expect to be able to POST an update method without having to add the nested castle id and have it just validate and work. That looks like it's fixed in the latest.
API call: POST /api/v1/rooms-update/

{
    "id": 1,
    "name": "Throne Room Changed Text",
}

TO RETURN

{
    "id": 1,
    "name": "Throne Room Changed Text",
    "castle": {
        "id": 1,
        "name": "Daventry Castle"
    }
}

It seems none of this is currently working. What am I missing out there (probably obvious to everyone else) to get this library working?

Add settings for the query parameter names

Now, the query parameter expand and fields are fixed. Also, the magic value ~all as expand value is fixed. It would be nicer if these could be configured in the settings.

Bug in serializer when passing params

Hi!
Thanks for the great library! I faced an issue with multiple nested fields expansion and was able to track it down to the following lines:

if name in nested_expand:
settings["expand"] = nested_expand[name]
if name in nested_fields:
settings["fields"] = nested_fields[name]
if name in nested_omit:
settings["omit"] = nested_omit[name]
if type(serializer_class) == str:
serializer_class = self._get_serializer_class_from_lazy_string(
serializer_class
)
return serializer_class(**settings)

TypeError: __init__() got an unexpected keyword argument 'expand'

Since I am using custom REST_FLEX_FIELDS params, this is throwing an error because the serializer expected parameters with different names since you only pop off the expected custom params

def __init__(self, *args, **kwargs):
expand = list(kwargs.pop(EXPAND_PARAM, []))
fields = list(kwargs.pop(FIELDS_PARAM, []))
omit = list(kwargs.pop(OMIT_PARAM, []))
super(FlexFieldsSerializerMixin, self).__init__(*args, **kwargs)

I believe all thats needed to fix this is to edit this section to utilize the custom params instead when passing

Automatic Query Optimization

is it possible to optimize query automatically for list request? I am not really sure how to implement this, but maybe we can override get_queryset to append the query with prefetch_related using fields that are is_expanded?

Expand Argument Not Respected When Empty

I would expect that when instantiating a serializer and explicitly setting the expand argument, that would be respected no matter what. Instead, the behavior is that if there is an expand query param, that is respected over an empty list argument because empty lists are falsy. Here is the logic in question:

"expand": (
                self._get_permitted_expands_from_query_param(EXPAND_PARAM)
                if not expand
                else []
            ),

Maybe this is the intended behavior but it seems strange to me. Feature or bug?

fields arguement in Serializer constructor not working from 0.8.0

This works perfectly fine in 0.7.5, but breaks from 0.8 ๐Ÿ˜Ÿ

from rest_flex_fields import FlexFieldsModelSerializer
from django.db import models


class MentorMentee(models.Model):
    mentee = models.ForeignKey('CustomUser', related_name="mentors", on_delete=models.CASCADE)
    skill = models.ForeignKey('Skill', on_delete=models.CASCADE)
    mentor = models.ForeignKey('CustomUser', related_name="mentees", on_delete=models.CASCADE)


class MentorReqSerializer(FlexFieldsModelSerializer):
    class Meta:
        model = MentorMentee
        fields = '__all__'


data = MentorReqSerializer(fields=('mentor', 'skill'), data={'mentor': 1, 'skill': 51518})
data.is_valid(raise_exception=True)

Raises

rest_framework.exceptions.ValidationError: {'mentee': [ErrorDetail(string='This field is required.', code='required')]}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.