Skip to content

Added support to filter nested fields #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ dist/
*.egg-info/
build/
.tox/
.idea
102 changes: 87 additions & 15 deletions drf_dynamic_fields/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
"""
Mixin to dynamically select only a subset of fields per DRF resource.
"""

import warnings

from django.conf import settings
from django.utils.functional import cached_property

from rest_framework import serializers


class DynamicFieldsMixin(object):
"""
A serializer mixin that takes an additional `fields` argument that controls
which fields should be displayed.
"""

@property
def is_preventing_nested_serializers(self):
is_root = self.root == self
parent_is_list_root = self.parent == self.root and getattr(
self.parent, "many", False
)

return not (is_root or parent_is_list_root)

@cached_property
def fields(self):
"""
Expand All @@ -29,13 +41,7 @@ def fields(self):
# We are being called before a request cycle
return fields

# Only filter if this is the root serializer, or if the parent is the
# root serializer with many=True
is_root = self.root == self
parent_is_list_root = self.parent == self.root and getattr(
self.parent, "many", False
)
if not (is_root or parent_is_list_root):
if self.is_preventing_nested_serializers:
return fields

try:
Expand All @@ -55,15 +61,11 @@ def fields(self):
if params is None:
warnings.warn("Request object does not contain query parameters")

try:
filter_fields = params.get("fields", None).split(",")
except AttributeError:
filter_fields = None
source = get_source_path(self)
level = compute_level(self)

try:
omit_fields = params.get("omit", None).split(",")
except AttributeError:
omit_fields = []
filter_fields = self.get_filter_fields(params.get("fields", None), level, source)
omit_fields = self.get_omit_fields(params.get("omit", None), level, source)

# Drop any fields that are not specified in the `fields` argument.
existing = set(fields.keys())
Expand All @@ -85,3 +87,73 @@ def fields(self):
fields.pop(field, None)

return fields

def get_filter_fields(self, params, level, source, default=None, include_parent=True):
try:
return params.split(",")
except AttributeError:
return default


def get_omit_fields(self, params, level, source):
return self.get_filter_fields(params, level, source, default=[], include_parent=False)


class NestedDynamicFieldsMixin(DynamicFieldsMixin):

@property
def is_preventing_nested_serializers(self):
return False

def get_filter_fields(self, params, level, source, default=None, include_parent=True):
fields = super().get_filter_fields(params, level, source, default, include_parent)
return get_fields_for_level_and_prefix(
fields,
level,
source,
default=default,
include_parent=include_parent
)

def get_source_path(serializer):
parts = []
current = serializer
while current.parent is not None:
if hasattr(current, 'field_name'):
parts.insert(0, current.field_name)
current = current.parent
return "__".join(filter(None, parts))

def get_fields_for_level_and_prefix(fields_list, level, source, include_parent, default):
if not fields_list:
return default

allowed = set()
prefix = source.split("__") if source else []
for f in fields_list:
parts = f.split("__")
if parts[:level] != prefix:
continue
if len(parts) <= level + 1:
allowed.add(parts[-1])
elif len(parts) > level + 1 and include_parent:
# include parent field to ensure nesting proceeds
allowed.add(parts[level])
if set(prefix) == allowed:
return default
return allowed

def compute_level(serializer):
level = 0
current = serializer
while hasattr(current, 'parent') and current.parent is not None:
parent = current.parent

# Handle ListSerializer by skipping over it
if isinstance(parent, serializers.ListSerializer):
current = parent.parent
else:
current = parent

level += 1
return level
9 changes: 9 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ class School(models.Model):

name = models.CharField(max_length=30)
teachers = models.ManyToManyField(Teacher)


class Child(models.Model):
secret = models.CharField(max_length=100)
public = models.CharField(max_length=100)


class Parent(models.Model):
child = models.ForeignKey(Child, on_delete=models.CASCADE)
48 changes: 37 additions & 11 deletions tests/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@
"""
from rest_framework import serializers

from drf_dynamic_fields import DynamicFieldsMixin
from drf_dynamic_fields import DynamicFieldsMixin, NestedDynamicFieldsMixin

from .models import Teacher, School
from .models import Teacher, School, Child


class TeacherSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
"""
The request_info field is to highlight the issue accessing request during
a nested serializer.
"""
class BaseTeacherSerializer(serializers.ModelSerializer):

request_info = serializers.SerializerMethodField()

Expand All @@ -29,14 +25,44 @@ def get_request_info(self, teacher):
return request.build_absolute_uri("/api/v1/teacher/{}".format(teacher.pk))


class SchoolSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
class TeacherSerializer(DynamicFieldsMixin, BaseTeacherSerializer):
pass


class NestableTeacherSerializer(NestedDynamicFieldsMixin, BaseTeacherSerializer):
"""
Interesting enough serializer because the TeacherSerializer
will use ListSerializer due to the `many=True`
The request_info field is to highlight the issue accessing request during
a nested serializer.

"""

teachers = TeacherSerializer(many=True, read_only=True)
class BaseSchoolSerializer(serializers.ModelSerializer):


class Meta:
model = School
fields = ("id", "teachers", "name")


class SchoolSerializer(DynamicFieldsMixin, BaseSchoolSerializer):
teachers = TeacherSerializer(many=True, read_only=True)


class NestableSchoolSerializer(NestedDynamicFieldsMixin, BaseSchoolSerializer):
"""
Interesting enough serializer because the TeacherSerializer
will use ListSerializer due to the `many=True`
"""
teachers = NestableTeacherSerializer(many=True, read_only=True)

class ChildSerializer(NestedDynamicFieldsMixin, serializers.Serializer):
secret = serializers.CharField()
public = serializers.CharField()

class Meta:
model = Child


class ParentSerializer(NestedDynamicFieldsMixin, serializers.Serializer):
id = serializers.IntegerField()
child = ChildSerializer()
Loading
Loading