drf-ext 0.1.1
Extensions for the DRF
Requires Python: >=3.6
Keywords: django django-rest django_rest rest development

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

```python

from drf_ext import NestedCreateUpdateMetaclass, update_error_dict

```

---

# Examples:

Assuming the following `models.py`:

```python

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`:

```python

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:

```python

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:

```python

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

```python

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`:

```python

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`:

```python

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

```python

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

		non_required_fields = ()

```
---

#### `common_field_params`:

```python
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:

```python

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:

```python

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:

```python

	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`:

```python

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`:

```python

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`:

```python

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

---