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.

Source Distribution

drf-ext-0.1.1.tar.gz (3.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

drf_ext-0.1.1-py3-none-any.whl (18.1 kB view details)

Uploaded Python 3

File details

Details for the file drf-ext-0.1.1.tar.gz.

File metadata

  • Download URL: drf-ext-0.1.1.tar.gz
  • Upload date:
  • Size: 3.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/46.0.0 requests-toolbelt/0.9.1 tqdm/4.40.2 CPython/3.8.0

File hashes

Hashes for drf-ext-0.1.1.tar.gz
Algorithm Hash digest
SHA256 d71fad8810c88171e2d94a4aec8ef672277f96f85d3d31498e5b1d4b95017ea9
MD5 0e0b0468bfc09731683df3918e2ccde0
BLAKE2b-256 111bef4d4dab8aea13d39eef3a3fc6e3fd1dc3cbd170b549171e500f08475d48

See more details on using hashes here.

File details

Details for the file drf_ext-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: drf_ext-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 18.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/46.0.0 requests-toolbelt/0.9.1 tqdm/4.40.2 CPython/3.8.0

File hashes

Hashes for drf_ext-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 1190b8dbeb5efeffe046201ce5f65f855dfad54cab01acb56d1e6d8d870f8f8f
MD5 6984bf08b1ef9df595571ae8db0a539f
BLAKE2b-256 21f7230200af1bd7e3cc1d36e39f02d40631082522dd1b34a15e1e7144a3be84

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page