From 9dd6a9140fd6d2ffe1baf9ab1ffc728540dea84d Mon Sep 17 00:00:00 2001 From: robert Date: Sat, 11 Mar 2023 07:28:25 -0600 Subject: [PATCH] Minor tweaks and updates version and release notes --- README.md | 14 ++++--- rest_flex_fields/__init__.py | 4 +- rest_flex_fields/serializers.py | 17 ++++---- setup.py | 2 +- tests/settings.py | 14 +++++-- tests/test_flex_fields_model_serializer.py | 45 +++++++++++++++------- 6 files changed, 64 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index e7725d5..929330d 100644 --- a/README.md +++ b/README.md @@ -486,7 +486,7 @@ Parameter names and wildcard values can be configured within a Django setting, n | Option | Description | Default | |-------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|-----------------| | EXPAND_PARAM | The name of the parameter with the fields to be expanded | `"expand"` | -| MAXIMUM_EXPANSION_DEPTH | The number of maximum depth permitted expansion | `None` | +| 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` | @@ -498,13 +498,13 @@ For example, if you want your API to work a bit more like [JSON API](https://jso REST_FLEX_FIELDS = {"EXPAND_PARAM": "include"} ``` -### Defining expansion and recursive limits at serializer level +### Defining Expansion and Recursive Limits on Serializer Classes -`maximum_expansion_depth` property can be overridden at serializer level. It can be configured as `int` or `None`. +A `maximum_expansion_depth` integer property can be set on a serializer class. -`recursive_expansion_permitted` property can be overridden at serializer level. It must be `bool`. +`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 overridden in `_recursive_expansion_found` and `_expansion_depth_exceeded` methods. +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 @@ -584,6 +584,10 @@ It will automatically call `select_related` and `prefetch_related` on the curren # 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! diff --git a/rest_flex_fields/__init__.py b/rest_flex_fields/__init__.py index 108f477..568d88b 100644 --- a/rest_flex_fields/__init__.py +++ b/rest_flex_fields/__init__.py @@ -6,7 +6,9 @@ FIELDS_PARAM = FLEX_FIELDS_OPTIONS.get("FIELDS_PARAM", "fields") OMIT_PARAM = FLEX_FIELDS_OPTIONS.get("OMIT_PARAM", "omit") MAXIMUM_EXPANSION_DEPTH = FLEX_FIELDS_OPTIONS.get("MAXIMUM_EXPANSION_DEPTH", None) -RECURSIVE_EXPANSION_PERMITTED = FLEX_FIELDS_OPTIONS.get("RECURSIVE_EXPANSION_PERMITTED", True) +RECURSIVE_EXPANSION_PERMITTED = FLEX_FIELDS_OPTIONS.get( + "RECURSIVE_EXPANSION_PERMITTED", True +) WILDCARD_ALL = "~all" WILDCARD_ASTERISK = "*" diff --git a/rest_flex_fields/serializers.py b/rest_flex_fields/serializers.py index ea9a1a0..f5d4179 100644 --- a/rest_flex_fields/serializers.py +++ b/rest_flex_fields/serializers.py @@ -2,7 +2,6 @@ import importlib from typing import List, Optional, Tuple -from django.conf import settings from rest_framework import serializers from rest_flex_fields import ( @@ -10,6 +9,8 @@ FIELDS_PARAM, OMIT_PARAM, WILDCARD_VALUES, + MAXIMUM_EXPANSION_DEPTH, + RECURSIVE_EXPANSION_PERMITTED, split_levels, ) @@ -65,7 +66,7 @@ def get_maximum_expansion_depth(self) -> Optional[int]: """ Defined at serializer level or based on MAXIMUM_EXPANSION_DEPTH setting """ - return self.maximum_expansion_depth or settings.REST_FLEX_FIELDS.get("MAXIMUM_EXPANSION_DEPTH", None) + return self.maximum_expansion_depth or MAXIMUM_EXPANSION_DEPTH def get_recursive_expansion_permitted(self) -> bool: """ @@ -74,7 +75,7 @@ def get_recursive_expansion_permitted(self) -> bool: if self.recursive_expansion_permitted is not None: return self.recursive_expansion_permitted else: - return settings.REST_FLEX_FIELDS.get("RECURSIVE_EXPANSION_PERMITTED", True) + return RECURSIVE_EXPANSION_PERMITTED def to_representation(self, instance): if not self._flex_fields_rep_applied: @@ -280,19 +281,19 @@ def _get_query_param_value(self, field: str) -> List[str]: values = self.context["request"].query_params.getlist(field) if not values: - values = self.context["request"].query_params.getlist("{}[]".format(field)) + values = self.context["request"].query_params.getlist(f"{field}[]") + + if values and len(values) == 1: + values = values[0].split(",") for expand_path in values: self._validate_recursive_expansion(expand_path) self._validate_expansion_depth(expand_path) - if values and len(values) == 1: - return values[0].split(",") - return values or [] def _split_expand_field(self, expand_path: str) -> List[str]: - return expand_path.split('.') + return expand_path.split(".") def recursive_expansion_not_permitted(self): """ diff --git a/setup.py b/setup.py index edd224b..61bba17 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def readme(): ] setup( name="drf-flex-fields", - version="1.0.1", + version="1.0.2", description="Flexible, dynamic fields and nested resources for Django REST Framework serializers.", author="Robert Singer", author_email="robertgsinger@gmail.com", diff --git a/tests/settings.py b/tests/settings.py index 7102ea3..edc2bc3 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -88,9 +88,15 @@ { "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",}, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, ] @@ -120,4 +126,4 @@ # 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' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/tests/test_flex_fields_model_serializer.py b/tests/test_flex_fields_model_serializer.py index 27fea58..0102a78 100644 --- a/tests/test_flex_fields_model_serializer.py +++ b/tests/test_flex_fields_model_serializer.py @@ -185,26 +185,33 @@ def test_import_serializer_class(self): def test_make_expanded_field_serializer(self): pass - @override_settings(REST_FLEX_FIELDS={"RECURSIVE_EXPANSION_PERMITTED": False}) + @patch("rest_flex_fields.serializers.RECURSIVE_EXPANSION_PERMITTED", False) def test_recursive_expansion(self): with self.assertRaises(serializers.ValidationError): FlexFieldsModelSerializer( context={ "request": MockRequest( - method="GET", query_params=MultiValueDict({"expand": ["dog.leg.dog"]}) + method="GET", + query_params=MultiValueDict({"expand": ["dog.leg.dog"]}), ) } ) - @patch('rest_flex_fields.FlexFieldsModelSerializer.recursive_expansion_permitted', new_callable=PropertyMock) - def test_recursive_expansion_serializer_level(self, mock_recursive_expansion_permitted): + @patch( + "rest_flex_fields.FlexFieldsModelSerializer.recursive_expansion_permitted", + new_callable=PropertyMock, + ) + def test_recursive_expansion_serializer_level( + self, mock_recursive_expansion_permitted + ): mock_recursive_expansion_permitted.return_value = False with self.assertRaises(serializers.ValidationError): FlexFieldsModelSerializer( context={ "request": MockRequest( - method="GET", query_params=MultiValueDict({"expand": ["dog.leg.dog"]}) + method="GET", + query_params=MultiValueDict({"expand": ["dog.leg.dog"]}), ) } ) @@ -214,43 +221,55 @@ def test_expansion_depth(self): serializer = FlexFieldsModelSerializer( context={ "request": MockRequest( - method="GET", query_params=MultiValueDict({"expand": ["dog.leg.paws"]}) + method="GET", + query_params=MultiValueDict({"expand": ["dog.leg.paws"]}), ) } ) self.assertEqual(serializer._flex_options_all["expand"], ["dog.leg.paws"]) - @override_settings(REST_FLEX_FIELDS={"MAXIMUM_EXPANSION_DEPTH": 2}) + @patch("rest_flex_fields.serializers.MAXIMUM_EXPANSION_DEPTH", 2) def test_expansion_depth_exception(self): with self.assertRaises(serializers.ValidationError): FlexFieldsModelSerializer( context={ "request": MockRequest( - method="GET", query_params=MultiValueDict({"expand": ["dog.leg.paws"]}) + method="GET", + query_params=MultiValueDict({"expand": ["dog.leg.paws"]}), ) } ) - @patch('rest_flex_fields.FlexFieldsModelSerializer.maximum_expansion_depth', new_callable=PropertyMock) + @patch( + "rest_flex_fields.FlexFieldsModelSerializer.maximum_expansion_depth", + new_callable=PropertyMock, + ) def test_expansion_depth_serializer_level(self, mock_maximum_expansion_depth): mock_maximum_expansion_depth.return_value = 3 serializer = FlexFieldsModelSerializer( context={ "request": MockRequest( - method="GET", query_params=MultiValueDict({"expand": ["dog.leg.paws"]}) + method="GET", + query_params=MultiValueDict({"expand": ["dog.leg.paws"]}), ) } ) self.assertEqual(serializer._flex_options_all["expand"], ["dog.leg.paws"]) - @patch('rest_flex_fields.FlexFieldsModelSerializer.maximum_expansion_depth', new_callable=PropertyMock) - def test_expansion_depth_serializer_level_exception(self, mock_maximum_expansion_depth): + @patch( + "rest_flex_fields.FlexFieldsModelSerializer.maximum_expansion_depth", + new_callable=PropertyMock, + ) + def test_expansion_depth_serializer_level_exception( + self, mock_maximum_expansion_depth + ): mock_maximum_expansion_depth.return_value = 2 with self.assertRaises(serializers.ValidationError): FlexFieldsModelSerializer( context={ "request": MockRequest( - method="GET", query_params=MultiValueDict({"expand": ["dog.leg.paws"]}) + method="GET", + query_params=MultiValueDict({"expand": ["dog.leg.paws"]}), ) } )