Skip to main content

Extensions for the DRF

Project description

DRF Ext (Django REST Framework Extensions)

Extensions for the DRF

Installation:

pip install drf-ext

Features/Extensions:

  • Nested model serializer saving (create/update)
  • Declaration of non-required fields
  • Add multiple common parameters to a set of fields
  • Check fields' existence on de-serialization (create/update)
  • Check any field's existence among a set of fields on de-serialization (create/update)
  • Several frequently used utilities

Available objects:

Metaclasses:

  • NestedCreateUpdateMetaclass: provides nested serializer writes on create and update.
  • FieldOptionsMetaclass: provides various field declaration options for different scenarios.
  • ExtendedSerializerMetaclass: contains both NestedCreateUpdateMetaclass and FieldOptionsMetaclass.
  • InheritableExtendedSerializerMetaclass: ExtendedSerializerMetaclass with inheritance support.

Mixins:

  • NestedCreateUpdateMixin: provides nested serializer writes on create and update (used by NestedCreateUpdateMetaclass), see example below.

Utilities:

  • update_error_dict: allows updating a ValidationError error dict with provided key/value.
  • exc_dict_has_keys: tests whether given key(s) are in the exception error dict (e.g. ValidationError).
  • get_request_user_on_serializer: gets the current user object from inside the serializer.

NOTE: All of the above are import-able from drf_ext e.g.:

from drf_ext import NestedCreateUpdateMetaclass, update_error_dict

Examples:

Assuming the following models.py:

from django.db import models
from django.contrib.auth.models import User


class Tag(models.Model):

	name = models.CharField(max_length=12)


class Address(models.Model):

	user = models.OneToOneField(
		User,
		on_delete=models.CASCADE,
		related_name="address",
		null=True,
		blank=True,
	)
	tags = models.ManyToManyField(Tag, related_name="addresses", blank=True)

	state = models.CharField(max_length=2)
	zip_code = models.CharField(max_length=12)


class Client(models.Model):

	user = models.OneToOneField(
		User,
		on_delete=models.CASCADE,
		related_name="client",
		null=False,
		blank=False,
	)

Metaclasses/mixins:

NestedCreateUpdateMetaclass/NestedCreateUpdateMixin:

serializers.py:

from rest_framework import serializers

from drf_ext import NestedCreateUpdateMetaclass


class AddressSerializer(serializers.ModelSerializer):
	class Meta:
		model = Address
		fields = "__all__"


class UserSerializer(
	serializers.ModelSerializer, metaclass=NestedCreateUpdateMetaclass
):
	address = AddressSerializer()

	class Meta:
		model = User
		fields = "__all__"

Sample POST request:

data = {
	"username": "my_username",
	"password": "my_password",
	"address": {
		"state": "CA",
		"zip_code": "12345",
		"tags": [1, 3, 7]  # `pk` of `Tag` objects
	}
}

client.post("/users/", data=data)

Sample PUT/PATCH request:

data = {
	"address": {
		"_pk": 2,  # `pk` of the related `address`
		"state": "CA",
		"zip_code": "12345",
		"tags": [9, 24, 56]
	}
}

client.patch("/users/1/", data=data)

NOTE: drf_ext uses the existence of _pk field to track whether it's a new nested object creation or an update. So if _pk is omitted it would taken as new nested object creation request.

This _pk write-only field is automatically injected to all nested serializers by the metaclass. But if one is using the NestedCreateUpdateMixin, they need to explicitly define the field on the nested serializer e.g.:

class AddressSerializer(serializers.ModelSerializer):

	_pk = serializers.IntegerField(write_only=True, required=False)  # here

	class Meta:
		model = Address
		fields = ("pk", "_pk", "state", "zip_code")
		read_only_fields = ("pk",)


class UserSerializer(NestedCreateUpdateMixin, serializers.ModelSerializer):

	address = AddressSerializer()

	class Meta:
		model = User
		fields = "__all__"

Everything else remains the same as NestedCreateUpdateMetaclass.

FieldOptionsMetaclass:

required_fields_on_create, required_fields_on_update, required_fields_on_create_any, required_fields_on_update_any:

class AddressSerializer(serializers.ModelSerializer, metaclass=FieldOptionsMetaclass):
	class Meta:
		model = Address
		fields = ("pk", "state", "zip_code")
		read_only_fields = ("pk",)

		# These fields are required on POST request i.e. on creation of object
		required_fields_on_create = ("state", "zip_code")

		# These fields are required on PUT/PATCH request i.e. on update of object
		required_fields_on_update = ("zip_code",)


class UserSerializer(
	serializers.ModelSerializer, metaclass=FieldOptionsMetaclass
):
	address = AddressSerializer()

	class Meta:
		model = User
		fields = (
			"pk", "address", "username", "email",
			"password", "first_name", "last_name",
		)
		read_only_fields = ("pk",)

		# At least one of these fields are required on POST request
		required_fields_on_create_any = ("first_name", "last_name")

		# At least one of these fields are required on PUT/PATCH request
		required_fields_on_update_any = ("address", "username", "email")

non_required_fields:

class AddressSerializer(serializers.ModelSerializer, metaclass=FieldOptionsMetaclass):
	class Meta:
		model = Address
		fields = ("pk", "state", "zip_code")
		read_only_fields = ("pk",)

		# The mentioned fields are made *non-required*, like providing
		# the `required=False` parameter on them. All fields are taken
		# as required, unless model has `blank=True` on the field, or
		# explicitly mentioned with `required=False`. This will allow
		# to control that from a single place. Also, this would make
		# working with `required_fields_on_create` and related options
		# (see above) easier to follow as users can decide to make a
		# field mandatory in POST but not in PUT/PATCH and vice versa,
		# which allows for a finer control over fields.
		non_required_fields = ("state", "zip_code")

NOTE: If non_required_fields is not provided, all fields mentioned in fields (without exclude-ed ones) are made non-required. To use the default option of DRF, one can set non_required_fields to an empty iterable e.g.:

class AddressSerializer(serializers.ModelSerializer, metaclass=FieldOptionsMetaclass):
	class Meta:
		model = Address
		fields = ("pk", "state", "zip_code")

		non_required_fields = ()

common_field_params:

class AddressSerializer(serializers.ModelSerializer, metaclass=FieldOptionsMetaclass):
	class Meta:
		model = Address
		fields = ("pk", "state", "zip_code")
		read_only_fields = ("pk",)

		# `common_field_params` allows to add some common parameters
		# to a set of fields. This must be a `dict` with the keys
		# being an (hashable) iterable e.g. `tuple` and values being
		# a `dict` of parameter-values.
		common_field_params = {
			("state", zip_code"): {
				"allow_blank": False,
				"trim_whitespace": True,
			},

			# Using a single field is also fine (this works similar
			# to the default `extra_kwargs` in that case).
			("state",): {
				"max_length": 2,
			},
		}

ExtendedSerializerMetaclass:

If you want to use all features from NestedCreateUpdateMetaclass and FieldOptionsMetaclass mentioned above, use this metaclass:

class UserSerializer(serializers.ModelSerializer, metaclass=ExtendedSerializerMetaclass):

	address = AddressSerializer()

	class Meta:
		model = Address
		fields = "__all__"

		required_fields_on_create = ("username, "password",)
		required_fields_on_update_any = ("first_name", "last_name", "email")

Sample POST request:

data = {
	"username": "my_username",
	"password": "my_password",
	"address": {
		"state": "CA",
		"zip_code": "12345"
	}
}

client.post("/users/", data=data)

InheritableExtendedSerializerMetaclass:

Works exactly like ExtendedSerializerMetaclass. This one should be used to include all the attributes defined in superclasses (ignoring the dunder and Meta attributes, and callables).

This is designed to be used instead of ExtendedSerializerMetaclass when e.g. a (common) base class contains field definitions that are to be inherited by all child classes. For example:

	class Common:
		field = serializers.IntegerField()

	class Serializer(
		serializers.ModelSerializer,
		metaclass=InheritableExtendedSerializerMetaclass
	):
		# `field` will be injected here like it were defined
		# on this class body.
		...

Utilities:

update_error_dict:

errors = {}

if ...:
	# Following will result in `errors` being:
	# `{"field": ["Error message"]}`
	update_error_dict(errors, "field", "Error message")

if ...:
	# Following will result in `errors` being:
	# `{"field": ["Error message", "New error message"]}`
	update_error_dict(errors, "field", "New error message")

if errors:
	raise ValidationError(errors)

exc_dict_has_keys:

exc = ValidationError({
	"field_1": ["msg", "new msg"],
	"field_2": ["msg"],
})

exc_dict_has_keys(exc, ("field_1", "field_2"))  # returns `True`
exc_dict_has_keys(exc, "field_1")  # returns `True`

exc_dict_has_keys(exc, ("field_1", "field_2", "field_3"))  # returns `False`
exc_dict_has_keys(exc, "field_3")  # returns `False`

get_request_user_on_serializer:

class MySerializer(serializers.Serializer):
	...
	...

	def create(self, validated_data):
		# Get the user sending this request
		user = get_request_user_on_serializer(self)

Development:

  • Install dev dependencies:

      pip install drf-ext[dev]
    
  • Run tests:

      drf_ext/tests$ PYTHONPATH=.. pytest
    

License:

MIT


Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Files for drf-ext, version 0.1.1
Filename, size File type Python version Upload date Hashes
Filename, size drf_ext-0.1.1-py3-none-any.whl (18.1 kB) File type Wheel Python version py3 Upload date Hashes View
Filename, size drf-ext-0.1.1.tar.gz (3.9 kB) File type Source Python version None Upload date Hashes View

Supported by

Pingdom Pingdom Monitoring Google Google Object Storage and Download Analytics Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN DigiCert DigiCert EV certificate StatusPage StatusPage Status page