Usage

Viewset defined serializers

Serializer class custom configs

By, default Django Rest Framework provides a method to get a serializer - get_serializer.
This checks if viewset instance has set serializer_class property and returuns its instance.
Audoma Extends this behavior by, extending the number of possible serializer class declarations.
First of all, you are allowed to define collect and response serializer classes for viewset.
Collect serializer will be used to collect and process request data.
Response serializers will be used to process data for the response.
Variable name pattern: common_{type}_serializer_class (type can be result or collect)
Example:
 1 from audoma.drf import viewsets
 2 from audoma.drf import mixins
 3 from example_app.serializers import (
 4     MyCollectSerializer,
 5     MyResultSerializer
 6 )
 7
 8 class MyViewSet(
 9     mixins.ActionModelMixin,
10     mixins.ListModelMixin,
11     viewsets.GenericViewSet
12 ):
13     common_collect_serializer_class = MyCollectSerializer
14     common_result_serializer_class = MyResultSerializer
Additionally audoma allows a definition of a custom serializer for each action in the viewset.
This is possible for generic drfs’ actions and also for custom actions,
created with @action decorator.
Variable name pattern: {action_name}_serializer_class
Example:
 1 from rest_framework.decorators import action
 2 from rest_framework.response import Response
 3
 4 from audoma.drf import viewsets
 5 from audoma.drf import mixins
 6 from example_app.serializers import (
 7     MyCreateSerializer,
 8     MyCustomActionSerializer
 9 )
10
11 class MyViewSet(
12     mixins.ActionModelMixin,
13     mixins.CreateModelMixin,
14     viewsets.GenericViewSet
15 )::
16     create_serializer_class = MyCreateSerializer
17     custom_action_serializer_class = MyCustomActionSerializer
18
19     @action(detail=True, methods=["post"])
20     def custom_action(self, request):
21         serializer = self.get_serializer(data=request.data)
22         serializer.is_valid(raise_exception=True)
23         serializer.save()
24         return Response(serializer.instance, status_code=200)
It is also possible for action to serve more than one HTTP method.
In audoma, it is allowed to assign different serializers for each of the actions HTTP methods.
Variable name pattern: {http_method}_{action_name}_serializer_class
Example:
 1 from audoma.drf import viewsets
 2 from audoma.drf import mixins
 3 from example_app.serializers import (
 4     MyCreateSerializer,
 5     MyCustomActionSerializer
 6 )
 7
 8 class MyViewSet(
 9     mixins.ActionModelMixin,
10     mixins.ListModelMixin,
11     viewsets.GenericViewSet
12 ):
13     get_list_serializer_class = MyListSerializer
14     post_list_serializer_class = MyBulkCreateSerializer
Back to collect and result serializers.
Each action may have defined different collect and result serializer classes.
Variable name pattern: {action_name}_{type}_serializer_class (type can be result or collect)
Example:
 1 from rest_framework.decorators import action
 2 from rest_framework.response import Response
 3
 4 from audoma.drf import viewsets
 5 from example_app.serializers import (
 6     MyCreateSerializer,
 7     MyCustomActionSerializer
 8 )
 9
10 class MyViewSet(
11     mixins.ActionModelMixin,
12     viewsets.GenericViewSet
13 ):
14     custom_action_collect_serializer = MyModelCreateSerializer
15     custom_action_result_serializer = MyModelSerializer
16
17     @action(detail=True, methods=["post"])
18     def custom_action(self, request):
19         serializer = self.get_serializer(data=request.data, serializer_type="collect")
20         serializer.is_valid(raise_exception=True)
21         serializer.save()
22         response_serializer = self.get_result_serializer(instance=serializer.instance)
23         return Response(response_serializer.data, status_code=201)
The most atomic way of defining serializer classes in audoma is to define serializer
per method, action and type.
This means that each action’s HTTP method will have result and collect serializer classes.
Variable name pattern: {htp_method}_{action_name}_{type}_serializer_class (type can be result or collect)
Example:
 1 from rest_framework.decorators import action
 2 from rest_framework.response import Response
 3
 4 from audoma.drf import viewsets
 5 from audoma.drf import mixins
 6 from example_app.serializers import (
 7     MyListSerializer,
 8     MySerializer,
 9     MyCreateSerializer
10 )
11
12 class MyViewSet(
13     mixins.ActionModelMixin,
14     mixins.ListModelMixin,
15     viewsets.GenericViewSet
16 ):
17     get_new_action_result_serializer_class = MyListSerializer
18     post_new_action_result_serializer_class = MySerializer
19     post_new_action_collect_serializer_class = MyCreateSerializer
20
21     @action(detail=True, methods=["post", "get"])
22     def new_action(self, request, *args, **kwargs):
23         if request.method == "POST":
24             serializer = self.get_serializer(data=request.data, serializer_type="collect")
25             serializer.is_valid(raise_exception=True)
26             serializer.save()
27             instance = serializer.instance
28         else:
29             instance = self.get_object()
30         response_serializer = self.get_result_serializer(instance=instance)
31         return Response(response_serializer.data, status_code=201)
As you surely presume, all of those serializer classes
variables may be defined on one viewset at once
Then those will be traversed in the defined order.
The first one matching will be used.
Let’s have a look at an example viewset:
 1 from rest_framework.decorators import action
 2 from rest_framework.response import Response
 3
 4 from audoma.drf import viewsets
 5 from example_app.serializers import (
 6     MySerachCollectSerializer,
 7     MySearchResultSerializer,
 8     MyCountCreateSerializer,
 9     MyCountUpdateSerializer,
10     MyCountResultSerializer,
11     MyDefaultSerializer
12 )
13 from example_app.models import (
14     MyModel,
15     CountModel
16 )
17
18
19 class MyViewSet(
20     mixins.ActionModelMixin,
21     mixins.CreateModelMixin,
22     mixins.RetrieveModelMixin,
23     mixins.DestroyModelMixin,
24     mixins.ListModelMixin,
25     viewsets.GenericViewSet,
26 ):
27
28     queryset = MyModel.objects.all()
29
30     post_search_collect_serializer_class = MySerachCollectSerializer
31     post_search_result_serializer_class = MySearchResultSerializer
32
33     post_count_collect_serializer_class = MyCountCreateSerializer
34     put_count_collect_serializer_class = MyCountUpdateSerializer
35     count_result_serializer_class = MyCountResultSerializer
36
37     serializer_class = MyDefaultSerializer
38
39     def get_object(self, pk=None):
40         return self.querset.get(pk=pk)
41
42     @action(detail=False, methods=["post"])
43     def search(self, request):
44         serializer = self.get_serializer(data=request.data, serializer_type="collect")
45         serializer.is_valid(raise_exception=True)
46         serializer.save()
47         result_serializer = self.get_result_serializer(instance=serializer.instance)
48         return Response(result_serializer.data, status=201)
49
50     @action(detail=True, methods=["post", "get", "put"])
51     def count(self, request, *args, **kwargs):
52         code = 200
53         if request.method != "GET":
54             serializer = self.get_serializer(data=request.data, serializer_type="collect")
55             serializer.is_valid(raise_exception=True)
56             serializer.save()
57             instance = serializer.instance
58             code = 201 if request.method == "POST"
59         else:
60             instance = CountModel.objects.get_count(slug=kwargs.pop("slug"))
61
62         result_serializer = self.get_result_serializer(instance=instance)
63         return Response(result_serializer.data, status=code)
Let’s examine the above example.
Action search has two serializers defined, both are defined for the POST method.
One of those will be used to collect data, the other to return the result.
In this case we may also simplify the serializer classes variable names,
because search only serves the POST method, so we may also name those variables like this:
...
search_collect_serializer_class = MySerachCollectSerializer
search_result_serializer_class = MySearchResultSerializer
...
This will work the same way as serializer classes defined in the example.
For the count action we have defined three serializers.
First two serializers handle collecting data for “POST and PUT HTTP methods.
The third serializer is common for all served by count HTTP methods, it is a result serializer.
No matter which method we will use, this is the serializer that will be used to return the result.
In this case, if there won’t be further changes in count action
we may define count_result_serializer_class as count_serializer_class.
This will work the same way because of the name traversing order defined in audoma.
But this solution may be problematic during introducing any changes.
...
post_count_collect_serializer_class = MyCountCreateSerializer
put_count_collect_serializer_class = MyCountUpdateSerializer
count_serializer_class = MyCountResultSerializer
...
The one last thing that is left in this viewset is serializer_class.
This variable will be used by all other actions supported by this viewset.
In the viewset definition there are few mixin classes passed, so those will
provide some basic functionalities to our viewset.
If this is going to be necessary it is possible to create a separate serializer for those actions also.
Example:
 1 from rest_framework.decorators import action
 2 from rest_framework.response import Response
 3
 4 from audoma.drf import viewsets
 5 from example_app.serializers import (
 6     MySerachCollectSerializer,
 7     MySearchResultSerializer,
 8     MyCountCreateSerializer,
 9     MyCountUpdateSerializer,
10     MyCountResultSerializer,
11     MyDefaultSerializer,
12     MyListSerializer,
13     MyCreateSerializer
14 )
15 from example_app.models import (
16     MyModel,
17     CountModel
18 )
19
20
21 class MyViewSet(
22     mixins.ActionModelMixin,
23     mixins.CreateModelMixin,
24     mixins.RetrieveModelMixin,
25     mixins.DestroyModelMixin,
26     mixins.ListModelMixin,
27     viewsets.GenericViewSet,
28 ):
29
30     queryset = MyModel.objects.all()
31
32     post_search_collect_serializer_class = MySerachCollectSerializer
33     post_search_result_serializer_class = MySearchResultSerializer
34
35     post_count_collect_serializer_class = MyCountCreateSerializer
36     put_count_collect_serializer_class = MyCountUpdateSerializer
37     count_result_serializer_class = MyCountResultSerializer
38
39     list_serializer_class = MyListSerializer
40     create_serializer_class = MyCreateSerializer
41     serializer_class = MyDefaultSerializer
42
43     def get_object(self, pk=None):
44         return self.querset.get(pk=pk)
45
46     @action(detail=False, methods=["post"])
47     def search(self, request):
48         serializer = self.get_serializer(data=request.data, serializer_type="collect")
49         serializer.is_valid(raise_exception=True)
50         serializer.save()
51         result_serializer = self.get_result_serializer(instance=serializer.instance)
52         return Response(result_serializer.data, status=201)
53
54     @action(detail=True, methods=["post", "get", "put"])
55     def count(self, request, *args, **kwargs):
56     code = 200
57         if request.method != "GET":
58             serializer = self.get_serializer(data=request.data, serializer_type="collect")
59             serializer.is_valid(raise_exception=True)
60             serializer.save()
61             instance = serializer.instance
62             code = 201 if request.method == "POST"
63         else:
64             instance = CountModel.objects.get_count(slug=kwargs.pop("slug"))
65
66         result_serializer = self.get_result_serializer(instance=instance)
67         return Response(result_serializer.data, status=code)

Serializer classes name traverse order

After examining the above examples, it is obvious that there is some defined order
while traversing defined variables. The variable which will be used as the serializer
class is being picked in this order:
  • {htp_method}_{action_name}_{type}_serializer_class (type can be result or collect)

  • {action_name}_{type}_serializer_class (type can be result or collect)

  • {http_method}_{action_name}_serializer_class

  • {action_name}_serializer_class

  • common_{type}_serializer_class (type can be result or collect)

  • serializer_class

For all serializers defined this way, there is also support for proper documentation in api schema.

Viewset defined headers

Since audoma 0.6.0 there is an easy way of returning custom headers for each action.
This allows easy adding custom header. This mechanism is quite simillar to what you probably know about serializer_class definition.
This works perfectly with audoma action.
Let’s say we have an action:
 1 ...
 2 @audoma_action(
 3     detail=True,
 4     methods=["post"],
 5     results=serializers.PerscriptionReadSerializer,
 6     errors=[models.Prescription.DoesNotExist],
 7     ignore_view_collectors=True,
 8 )
 9 def make_prescription_invalid(self, request, pk=None):
10     instance = self.get_object()
11     instance.is_valid = False
12     instance.save()
13     return instance, 200
While we are using audoma_action we are not simply able to pass headers to response,
because this decorator handles creating Response automatically.
To allow you to define your custom header we introduced get methods for headers.
Those methods names should follow this patterns:
  • get_{action}_{request_method}_response_headers - return headers for given action and HTTP method

  • get_{action}_response_headers - return headers for all of given action methods

  • get_{request_method}_response_headers - return headers for all HTTP request with given method

  • get_response_headers - return headers for all viewset actions

Those methods are being traversed in order defined above, the first one wich exist will
be used to retrieve headers for audoma_action response.
What’s more now audoma_action automatically adds Location header to the response with proper status code.
To make your Location header work properly you have to define URL_FIELD_NAME in your project settings.
If you want to learn more about Location header visit: Link

Permissions

By default, in the drf-spectacular viewset permissions were not documented at all. In audoma, permissions are being documented for each viewset separately.

You don’t have to define anything extra, this is being handled just out of the box. The only thing it is required is to define permissions on your viewset.

Example:

 1 from rest_framework.decorators import action
 2 from rest_framework.response import Response
 3
 4 from audoma.drf import viewsets
 5 from example_app.serializers import (
 6     MySerachCollectSerializer,
 7     MySearchResultSerializer,
 8     MyCountCreateSerializer,
 9     MyCountUpdateSerializer,
10     MyCountResultSerializer,
11     MyDefaultSerializer,
12     MyListSerializer,
13     MyCreateSerializer
14 )
15 from example_app.permissions import (
16     AlternatePermission1,
17     AlternatePermission2,
18     DetailPermission,
19     ViewAndDetailPermission,
20     ViewPermission,
21 )
22 from example_app.models import (
23     MyModel,
24     CountModel
25 )
26
27
28 class MyViewSet(
29     mixins.ActionModelMixin,
30     mixins.CreateModelMixin,
31     mixins.RetrieveModelMixin,
32     mixins.DestroyModelMixin,
33     mixins.ListModelMixin,
34     viewsets.GenericViewSet,
35 ):
36     permission_classes = [
37         IsAuthenticated,
38         ViewAndDetailPermission,
39         DetailPermission,
40         ViewPermission,
41         AlternatePermission1 | AlternatePermission2,
42     ]
43
44     queryset = MyModel.objects.all()
45
46     post_search_collect_serializer_class = MySerachCollectSerializer
47     post_search_result_serializer_class = MySearchResultSerializer
48
49     post_count_collect_serializer_class = MyCountCreateSerializer
50     put_count_collect_serializer_class = MyCountUpdateSerializer
51     count_result_serializer_class = MyCountResultSerializer
52
53     list_serializer_class = MyListSerializer
54     create_serializer_class = MyCreateSerializer
55     serializer_class = MyDefaultSerializer
56
57     def get_object(self, pk=None):
58         return self.querset.get(pk=pk)
59
60     @action(detail=False, methods=["post"])
61     def search(self, request):
62         serializer = self.get_serializer(data=request.data, serializer_type="collect")
63         serializer.is_valid(raise_exception=True)
64         serializer.save()
65         result_serializer = self.get_result_serializer(instance=serializer.instance)
66         return Response(result_serializer.data, status=201)
67
68     @action(detail=True, methods=["post", "get", "put"])
69     def count(self, request, *args, **kwargs):
70     code = 200
71         if request.method != "GET":
72             serializer = self.get_serializer(data=request.data, serializer_type="collect")
73             serializer.is_valid(raise_exception=True)
74             serializer.save()
75             instance = serializer.instance
76             code = 201 if request.method == "POST"
77         else:
78             instance = CountModel.objects.get_count(slug=kwargs.pop("slug"))
79
80         result_serializer = self.get_result_serializer(instance=instance)
81         return Response(result_serializer.data, status=code)
Currently there is no way to customize this behavior in audoma, also it is
not possible to disable permissions documentation.

Custom choices

Audoma provides a new way of defining choices and new choices class
which allows calling choice by its name.
Example definition and usage:
 1 from audoma.django.db import models
 2 from audoma.choices import make_choices
 3
 4
 5 class CarModel(models.Model):
 6
 7
 8     CAR_BODY_TYPES = make_choices(
 9         "BODY_TYPES",
10         (
11             (1, "SEDAN", "Sedan"),
12             (2, "COUPE", "Coupe"),
13             (3, "HATCHBACK", "Hatchback"),
14             (4, "PICKUP", "Pickup Truck"),
15         ),
16     )
17
18     name = models.CharField(max_length=255)
19     body_type = models.IntegerField(choices=CAR_BODY_TYPES.get_choices())
20
21     engine_size = models.FloatField()
22
23     def is_sedan(self):
24         return self.body_type is BODY_TYPE_CHOICES.SEDAN
Additionally it’s worth mentioning that those choices will be shown in docs in the fields description.
Those will also appear in the schema as x-choices.

Filters

Default Filters

In drf, it’s possible to define filterset_fields and filterset_class.
By default, drf-spectacular` supports django-filters. Which are being documented.
Audoma has been tested with the default DRFs filter backend and django_filters.rest_framework.DjangoFilterBackend.
For more accurate documentation, we recommend using django_filters.rest_framework.DjangoFilterBackend as the default one.
Filters and search fields are being documented out of the box.
Example:
 1 from rest_framework.filters import SearchFilter
 2 from audoma.drf import mixins
 3 from audoma.drf import viewsets
 4 from django_filters import rest_framework as df_filters
 5
 6 from example_app.models import CarModel
 7 from example_app.serializers import CarModelSerializer
 8
 9 class CarViewSet(
10     mixins.ActionModelMixin,
11     mixins.RetrieveModelMixin,
12     mixins.ListModelMixin,
13     viewsets.GenericViewSet,
14 ):
15     queryset = CarModel.objects.all()
16     serializer_class = CarModelSerializer
17
18     filter_backends = [SearchFilter, df_filters.DjangoFilterBackend]
19
20     filterset_fields = ["body_type"]
21     search_fields = ["=manufacturer", "name"]
It is also possible to define the filterset class which will also be documented
without any additional steps.
 1 from rest_framework.filters import SearchFilter
 2 from audoma.drf import mixins
 3 from audoma.drf import viewsets
 4 from django_filters import rest_framework as df_filters
 5
 6 from example_app.models import CarModel
 7 from example_app.serializers import CarModelSerializer
 8
 9
10 class CarFilter(df_filters.FilterSet):
11     body_type = df_filters.TypedChoiceFilter(
12         Car.CAR_BODY_TYPES.get_choices(), "body_type",
13         lookup_expr="exact", field_name="body_type"
14     )
15
16     class Meta:
17         model = CarModel
18         fields = [
19             "body_type",
20         ]
21
22
23 class CarViewSet(
24     mixins.ActionModelMixin,
25     mixins.RetrieveModelMixin,
26     mixins.ListModelMixin,
27     viewsets.GenericViewSet,
28 ):
29     queryset = CarModel.objects.all()
30     serializer_class = CarModelSerializer
31
32     filter_backends = [SearchFilter, df_filters.DjangoFilterBackend]
33
34     filterset_class = CarFilter
35     search_fields = ["=manufacturer", "name"]
Audoma extends documenting filters with two main features.
Additional enum documentation in field description:
In drf-spectacular, enums are being shown only as values possible to pass to the filter.
With audoma, you also get a display value of enum field.
This is being shown as: * api value - display value
The next feature is schema extension which is not visible in OpenApi frontend.
This schema extension is x-choices. Which provides mapping for filter values.
Passing x-choices in schema allows frontend developers to use mapping
to show display/value fields without looking into a field description.

Validators

ExclusiveFieldsValidator

This is an additional validator, which allows defining mutually exclusive fields in the serializer.
It validates if any of the fields have been given and if not all exclusive fields have been given.
This validator takes params:
  • fields - list or a tuple of field names

  • message - string message, which will replace the default validator message

  • required - boolean which determines if any of the fields must be given

  • message_required - a message which will be displayed if one of the fields is required, and none has been passed

Usage is simple:
 1 from audoma.drf import serializers
 2 from audoma.drf.validators import ExclusiveFieldsValidator
 3
 4
 5 class MutuallyExclusiveExampleSerializer(serializers.Serializer):
 6     class Meta:
 7         validators = [
 8             ExclusiveFieldsValidator(
 9                 fields=[
10                     "example_field",
11                     "second_example_field",
12                 ]
13             ),
14         ]
15
16     example_field = serializers.CharField(required=False)
17     second_example_field = serializers.CharField(required=False)

Decorators

@extend_schema_field

This decorator is by default drf-spectacular feature.
Audoma only changes its behavior, in drf-spectacular using this decorator causes overriding
all informations about the field. Audoma does not override information, it only updates available information
with those passed to the decorator.
This may be very useful while defining examples.
We don’t want to erase all other field information
just because we want to define an example for this field.
Also passing all field information additionally just because we want
to define an example seems unnecessary and redundant.
Example:
 1 from audoma.drf.fields import FloatField
 2
 3 from drf_spectacular.utils import extend_schema_field
 4
 5 @extend_schema_field(
 6     field={
 7         "example": 10.00
 8     }
 9 )
10 class CustomExampleFloatField(FloatField):
11     pass
Above we simply add a default example for all
fields which will be of class CustomExampleFloatField.

@audoma_action

DRFs action docs <https://www.django-rest-framework.org/api-guide/viewsets/#marking-extra-actions-for-routing>
This is one of the most complex features offered by audoma, an extension of an action decorator.
Decorator by default is Django Rest Framework functionality.
It also allows registering custom action for viewset.
In the case of audoma_action, it changes a bit how the action function should work,
using audoma_action action function should not return a Response object, it should return
tuple of instance and status code, audoma_action will take care of creating response out of it.

How to use this?

Let’s take an example viewset:
 1 from audoma.drf import mixins
 2 from audoma.drf import viewsets
 3
 4 from app.serializers import (
 5     CarListSerializer,
 6     CarWriteSerializer,
 7     CarDetailsSerializer,
 8     CarCreateRateSerializer,
 9     CarRateSerializer
10 )
11 from app.models import (
12     Car,
13     CarRate
14 )
15
16
17 class CarViewSet(
18     mixins.ActionModelMixin,
19     mixins.CreateModelMixin,
20     mixins.RetrieveModelMixin,
21     mixins.ListModelMixin,
22     viewsets.GenericViewSet,
23 ):
24
25     permission_classes = [
26         IsAuthenticated,
27         ViewAndDetailPermission,
28         DetailPermission,
29         ViewPermission,
30         AlternatePermission1 | AlternatePermission2,
31     ]
32
33     create_collect_serializer_class = CarWriteSerializer
34     create_result_serializer_class = CarDetailsSerializer
35     retrieve_serializer_class = CarDetailsSerializer
36     list_serializer_class = CarListSerializer
37
38     queryset = {}
39     @audoma_action(
40         detail=True,
41         methods=["get", "post"]
42         collectors=CarCreateRateSerializer,
43         results=CarRateSerializer,
44         errors=[CustomCarRateException]
45     )
46     def rate(self, request, pk=None, *args, **kwargs):
47         if request.method == "POST":
48             collect_serializer = kwargs.pop("collect_serializer")
49             instance = collect_serializer.save()
50         else:
51             instance = CarRate.objects.get_random_car_rate(car_pk=pk)
52         return instance, 200
Let’s examine the above example.
We’ve created the viewset with some initial actions served, and serializers assigned to those actions.
Next we’ve defined a new custom action called rate.
This action serves get and post methods, in case of this action ‘
we use a single result and collect serializers.
As you may see, audoma_action method does not return the default response, it returns
instance and status_code, the audoma_action decorator takes care
of creating the response from this.
Let’s modify our example, let there be a custom exception raised.
 1 from audoma.drf import mixins
 2 from audoma.drf import viewsets
 3 from rest_framework.exceptions import APIException
 4
 5 from app.serializers import (
 6     CarListSerializer,
 7     CarWriteSerializer,
 8     CarDetailsSerializer,
 9     CarCreateRateSerializer,
10     CarRateSerializer
11 )
12 from app.models import (
13     Car,
14     CarRate
15 )
16
17
18 class CustomCarRateException(APIException):
19     default_detail = "Error during retrieving car rate!"
20     status_code = 500
21
22
23 class CarViewSet(
24     mixins.ActionModelMixin,
25     mixins.CreateModelMixin,
26     mixins.RetrieveModelMixin,
27     mixins.ListModelMixin,
28     viewsets.GenericViewSet,
29 ):
30
31     permission_classes = [
32         IsAuthenticated,
33         ViewAndDetailPermission,
34         DetailPermission,
35         ViewPermission,
36         AlternatePermission1 | AlternatePermission2,
37     ]
38
39     create_collect_serializer_class = CarWriteSerializer
40     create_result_serializer_class = CarDetailsSerializer
41     retrieve_serializer_class = CarDetailsSerializer
42     list_serializer_class = CarListSerializer
43
44     queryset = {}
45
46     @audoma_action(
47         detail=True,
48         methods=["get", "post"]
49         collectors=CarCreateRateSerializer,
50         results=CarRateSerializer,
51         errors=[CustomCarRateException]
52     )
53     def rate(self, request, pk=None, *args, **kwargs):
54         if request.method == "POST":
55             collect_serializer = kwargs.pop("collect_serializer")
56             instance = collect_serializer.save()
57         else:
58             instance = CarRate.objects.get_random_car_rate(car_pk=pk)
59             if not instance:
60                 raise CustomCarRateException
61         return instance, 200
After this change it is possible to raise any exception of type CustomCarRateException in rate action.
Also this exception will be documented in this action schema.
Let’s presume that we now want to return status code 201 and rate instance on post,
but on get we want to return the car instance with random rate and status code 200.
 1 from audoma.drf import mixins
 2 from audoma.drf import viewsets
 3 from rest_framework.exceptions import APIException
 4
 5 from app.serializers import (
 6     CarListSerializer,
 7     CarWriteSerializer,
 8     CarDetailsSerializer,
 9     CarCreateRateSerializer,
10     CarRateSerializer
11 )
12 from app.models import (
13     Car,
14     CarRate
15 )
16
17
18 class CustomCarException(APIException):
19     default_detail = "Car can't be found"
20     status_code = 500
21
22
23 class CarViewSet(
24     mixins.ActionModelMixin,
25     mixins.CreateModelMixin,
26     mixins.RetrieveModelMixin,
27     mixins.ListModelMixin,
28     viewsets.GenericViewSet,
29 ):
30
31     permission_classes = [
32         IsAuthenticated,
33         ViewAndDetailPermission,
34         DetailPermission,
35         ViewPermission,
36         AlternatePermission1 | AlternatePermission2,
37     ]
38
39     create_collect_serializer_class = CarWriteSerializer
40     create_result_serializer_class = CarDetailsSerializer
41     retrieve_serializer_class = CarDetailsSerializer
42     list_serializer_class = CarListSerializer
43
44     queryset = {}
45
46     @audoma_action(
47         detail=False,
48         methods=["get", "post"]
49         collectors=CarCreateRateSerializer,
50         results={"post":{201: CarRateSerializer}, "get":{200: CarDetailsSerializer}},
51         errors=[CustomCarException]
52     )
53     def rate(self, request, *args, **kwargs):
54         if request.method == "POST":
55             collect_serializer = kwargs.pop("collect_serializer")
56             instance = collect_serializer.save()
57             return instance. 201
58         else:
59             instance = car.objects.get(pk=pk)
60             if not instance:
61                 raise CustomCarException
62             return instance, 200
Now we use different a serializer for each method, depending on returned status code.
Each of this serializer is using different model, audoma_action makes such situations super easy.
Let’s take a different example, we have an action that should return a string message, depending on
current car state.
 1 from audoma.drf import mixins
 2 from audoma.drf import viewsets
 3 from rest_framework.exceptions import APIException
 4
 5 from app.serializers import (
 6     CarListSerializer,
 7     CarWriteSerializer,
 8     CarDetailsSerializer,
 9     CarCreateRateSerializer,
10     CarRateSerializer
11 )
12 from app.models import (
13     Car,
14     CarRate
15 )
16
17
18 class CustomCarException(APIException):
19     default_detail = "Car can't be found"
20     status_code = 500
21
22
23 class CarViewSet(
24     mixins.ActionModelMixin,
25     mixins.CreateModelMixin,
26     mixins.RetrieveModelMixin,
27     mixins.ListModelMixin,
28     viewsets.GenericViewSet,
29 ):
30
31     permission_classes = [
32         IsAuthenticated,
33         ViewAndDetailPermission,
34         DetailPermission,
35         ViewPermission,
36         AlternatePermission1 | AlternatePermission2,
37     ]
38
39     create_collect_serializer_class = CarWriteSerializer
40     create_result_serializer_class = CarDetailsSerializer
41     retrieve_serializer_class = CarDetailsSerializer
42     list_serializer_class = CarListSerializer
43
44     queryset = {}
45
46     @audoma_action(
47         detail=False,
48         methods=["get", "post"]
49         collectors=CarCreateRateSerializer,
50         results={"post":{201: CarRateSerializer}, "get":{200: CarDetailsSerializer}},
51         errors=[CustomCarException]
52     )
53     def rate(self, request, *args, **kwargs):
54         if request.method == "POST":
55             collect_serializer = kwargs.pop("collect_serializer")
56             instance = collect_serializer.save()
57             return instance. 201
58         else:
59             instance = car.objects.get(pk=pk)
60             if not instance:
61                 raise CustomCarException
62             return instance, 200
63
64
65     @audoma_action(
66         detail=False,
67         methods=["get"],
68         results="Car is available"
69     )
70     def active(self, request, pk=None):
71         instance = self.get_object(pk=pk)
72         if instance.active:
73             return None, 200
74         return "Car is unavailable", 200
This action may return None or string, but as you may see in the results we have also string defined.
The string default in the results is a string that will be the message returned by default.
The default message will be returned if the instance is None.
If returned string instance won’t be None, then the returned instance will be
included in the response.
While returning string message as an instance, audoma simply wraps this message into json.
Wrapped message would look like this:
{
    "message": "Car is available"
}
We can combine those results, so in one action
we may return string instance and model instance.
Let’s modify our rate function, so it’ll return the default message if the rating is disabled.
 1 from audoma.drf import mixins
 2 from audoma.drf import viewsets
 3 from rest_framework.exceptions import APIException
 4 from django.conf import settings
 5
 6 from app.serializers import (
 7     CarListSerializer,
 8     CarWriteSerializer,
 9     CarDetailsSerializer,
10     CarCreateRateSerializer,
11     CarRateSerializer
12 )
13 from app.models import (
14     Car,
15     CarRate
16 )
17
18
19 class CustomCarException(APIException):
20     default_detail = "Car can't be found"
21     status_code = 500
22
23
24 class CarViewSet(
25     mixins.ActionModelMixin,
26     mixins.CreateModelMixin,
27     mixins.RetrieveModelMixin,
28     mixins.ListModelMixin,
29     viewsets.GenericViewSet,
30 ):
31
32     permission_classes = [
33         IsAuthenticated,
34         ViewAndDetailPermission,
35         DetailPermission,
36         ViewPermission,
37         AlternatePermission1 | AlternatePermission2,
38     ]
39
40     create_collect_serializer_class = CarWriteSerializer
41     create_result_serializer_class = CarDetailsSerializer
42     retrieve_serializer_class = CarDetailsSerializer
43     list_serializer_class = CarListSerializer
44
45     queryset = {}
46
47     @audoma_action(
48         detail=False,
49         methods=["get", "post"]
50         collectors=CarCreateRateSerializer,
51         results={
52             "post":{201: CarRateSerializer},
53             "get":{200: CarDetailsSerializer, 204:"Rate service currently unavailable"}
54         },
55         errors=[CustomCarException]
56     )
57     def rate(self, request, *args, **kwargs):
58         if settings.RATE_AVAILABLE:
59             return None, 204
60
61         if request.method == "POST":
62             collect_serializer = kwargs.pop("collect_serializer")
63             instance = collect_serializer.save()
64             return instance. 201
65         else:
66             instance = car.objects.get(pk=pk)
67             if not instance:
68                 raise CustomCarException
69             return instance, 200
70
71
72     @audoma_action(
73         detail=False,
74         methods=["get"],
75         results="Car is available"
76     )
77     def active(self, request, pk=None):
78         instance = self.get_object(pk=pk)
79         if instance.active:
80             return None, 200
81         return "Car is unavailable", 200

Params

Decorator audoma_action takes all params which may be passed to the action decorator.
It also takes additional params, which we will describe below:
collectors
This param allows defining serializer class which will collect and process request data.
To define this, action must serve POST/PATCH or PUT method, otherwise
defining those will cause an exception.
Collectors may be passed as:
  • Serializer class which must inherit from serializers.BaseSerializer

    @audoma_action(
        detail=False,
        methods=["post"],
        results=ExampleOneFieldSerializer,
        collectors=ExampleOneFieldSerializer,
    )
    
  • A dictionary with HTTP methods as keys and serializer classes as values. This allows defining different collector for each HTTP method.

    @audoma_action(
        detail=True,
        methods=["post"],
        collectors={"post": ExampleModelCreateSerializer},
        results=ExampleModelSerializer,
    )
    
If you are using PATCH or PUT method for your action, you may ask how to pass an instance
to your collect serializer. You simply have to override get_object method on your viewset, and make
it return the object you want to pass to collect serializer as an instance for given action and method.

Note

Passing collectors is optional, so you don’t have to pass them.
If collectors won’t be passed, and request method will be in [PUT, POST, PATCH]
then by default, audoma_action fill fallback to default
get_serializer_class method for audoma.

Note

If you are using collectors it is important to remember,
that your action method should accept additional kwarg collect_serializer
which will be a validated collector instance.
results
This param allows defining custom results for each method and each response status code.
Results param may be passed as:
  • Serializer class or which must inherit from serializers.BaseSerializer or string variable In this case, the serializer class passed will be used to produce every response coming from this action.

    @audoma_action(
        detail=True,
        methods=["put", "patch"],
        collectors=ExampleModelCreateSerializer,
        results=ExampleModelSerializer,
    )
    
  • A dictionary with HTTP methods as keys and serializer classes or string variables as values. In This case, there will be a different response serializer for each HTTP method.

    @audoma_action(
        detail=False,
        methods=["get", "post"],
        collectors={"post": MyCreateSerializer},
        results={"post": MySerializer, "get": MyListSerializer}
    )
    
  • A dictionary with HTTP methods as keys and dictionaries as values. Those dictionaries have status codes as keys and serializer classes or string variables as values.

    @audoma_action(
        detail=False,
        methods=["post"],
        collectors={"post": MyCreateSerializer},
        results={"post": {201: MySerializer, 204: MyNoContentSerializer}}
    )
    

Note

Results param is not mandatory, if you won’t pass the results
param into audoma_action, then there will be a fallback to default
errors
This param is a list of classes and instances of exceptions,
which are allowed to rise in this action.
Such behavior prevents rising, not defined exceptions, and allows to document
exceptions properly in OpenApi schema.
The main difference between passing exception class and exception instance, is that
if you pass exception instance, audoma will not only check if exception
type matches, it’ll also validate its content.
We presume that if you pass, the exception class, you want to accept all exceptions of this class.
In case the risen exception is not defined in audoma_action errors, there will be another
exception risen: AudomaActionException, in case the settings.DEBUG = False, this exception
will be handled silently by logging it, but the code will pass.
In the case of settings.DEBUG = True, then the exception won’t be silent.
By default audoma accepts some exceptions, which are defined globally.
Those exceptions are:
  • NotFound

  • NotAuthenticated

  • AuthenticationFailed

  • ParseError

  • PermissionDenied

If you want to extend this list of globally accepted exceptions, you can do it by
defining COMMON_API_ERRORS in your settings, for example:
COMMON_API_ERRORS = [
    myexceptions.SomeException
]

Note

Errors param is optional, but if they won’t be passed, action will only
allow rising globally defined exceptions.
ignore_view_collectors
Boolean variable which tells if audoma_action should fallback to
default way of retrieving collector from view, if the collector has not been passed
and action use method which allows collecting serializer usage.
many
This param decides if the returned instance should be treated as many by a serializer
Currently it can only be set to the concrete action, it is impossible to return a instance and
multiple instances from one action method using audoma_action.
run_get_object

Boolean variable which defines if get_object should be run to retrieve instance. If this has not been passed it’s being set depending on detail param for default action decorator.

Setting this to True for non detail view allows to force run get_object. This will be done in audoma_action, retrieved instance will be passed to collect_serializer

Examples

Define an example for the field

Above we described @extend_schema_field decorator which allows defining example for the field.
For all fields defined in audoma, there are examples generated automatically,
but you may also pass your example as a field parameter.
Example:
from audom.drf import serializers

class SalesContactSerializer(serializers.Serializer):
    phone_number = serializers.PhoneNumberField(example="+48 123 456 789")
    name = serializers.CharField(example="John", max_length=255)
After passing the example, it’ll be the value shown in example requests in docs.

Define custom fields with auto-generated examples

If you want to define your field with auto example generation,
it is possible, that your field class should inherit from the base ExampleMixin class,
set proper example class.
from rest_framework import fields
from audoma.mixins import ExampleMixin
from audoma.examples import NumericExample,


class SaleAmountField(ExampleMixin, fields.Field):
    audoma_example_class = NumericExample

Define custom example classes

It is possible to define your custom example classes, by default audio has defined
two specific example classes inside the audoma.examples module:
  • NumericExample

  • RegexExample

  • DateExample

  • TimeExample

  • DateTimeExample

  • Base64Example

  • RangeExample

And one base class:
  • Example

To define your example class, you should inherit from the Example class
and override the generate_value method
from audoma.examples import Example

class SaleExample(Example):
    def generate_value(self):
        return f"{self.amount} $"

Extra Fields

Money Field

Our money field is an extension of the MoneyField known from django_money.
This field is defined as one field in the model, but it creates two fields in the database.
There is nothing complex in this field usage, simply define it in your model:
from audoma.django.db import models

class SalesmanStats(models.Model):
    salesman = models.ForeignKey("sale.Salesman"e, on_delete=models.CASCADE)
    earned = models.MoneyField(max_digits=14, decimal_places=2, default_currency="PLN")
Field defined on the model required passing to it two variables.
Currency and amount, in our case we have set the default currency, so passing currency is not obligatory.
Those values may be passed in a few ways:
stats = SalesmanStats.objects.get(id=20)
# Simply pass the Money object
stats.earned = Money("99900.23", "PLN")
# You may also pass those variables to objects.create separately
sales = Salesman.objects.get(id=1)
stats = SalesmanStats.objects.create(
    salesman=sales, earned_amount=120,
    earned_courrency="PLN"
)
# In our case we defined the default currency, so it also may be
stats = SalesmanStats.objects.create(
    salesman=sales, earned_amount=120
)
# To get the amount we type
print(stats.earned) # this will print 120
print(stats.earned.currency) # will print PLN

PhoneNumberField

django-phonenumber-field docs

Audoma provides a PhoneNumberField which is an extension of the django-phonenumber-field.
You can use it in your models straight away, just as the original PhoneNumberField,
and what we added here is an automatically generated example in documentation,
based on country code.
Example:
from audoma.django.db import models

class SalesmanStats(models.Model):
    salesman = models.ForeignKey("sale.Salesman", on_delete=models.CASCADE)
    earned = models.MoneyField(max_digits=14, decimal_places=2, default_currency="PLN")
    phone_number = models.PhoneNumberField(region="GB")
The above code will result in the following example in the documentation:
{
    "salesman": 1,
    "earned": 500,
    "phone_number": "+44 20 7894 5678",
}

SerializerMethodField

This field is a common drf field, but in audoma its functionalities has been extended.
The most important feture is that now you are able to pass field param to SerializerMethodField constructor.
class DoctorWriteSerializer(PersonBaseSerializer):

specialization = serializers.SerializerMethodField(
    field=serializers.ListField(child=serializers.IntegerField()), writable=True
)

def get_specialization(self, doctor):
    return doctor.specialization.values_list("id", flat=True)

class Meta:
    model = api_models.Doctor
    fields = ["name", "surname", "contact_data", "specialization", "salary"]
Field param defines the structure, of returned value.
Now the value return by your method will be parsed by passed field to_representation method.
This also provides proper documenting such fields, there is no more necessity using @document_and_format
on each serializer method, to be sure that proper type will be display in docs, now it’ll be done automatically.
The next great thing about our custom SerializerMethodField is that it accepts writable param.
As you may suspect this param is a boolean value which may make this field writable.
During writting to this field it’ll behave as field passed to it with field param, so the validation,
and parsing to internal value depends on the passed field.

Note

It is not possible to define writable SerializerMethodField with no child field passed. Such behaviour will cause an exception.

Postgres fields

Django has defined in itself postgres specific fields. Since 0.6.0 audoma fully support those fields, it supports documentation and automatic behaviours for those fields.

To support those fields audoma uses external package: drf-extra-fields Audoma extends those package basic functionalities by adding automatic field mapping for ModelSerializer. And providing specific examples generated for each fields, which makes docs more readable.

Schema Extensions

x-choices

This extension is being added to all fields schema which have limited choice to some range.
All fields which have defined choices as enum will have this included in their schema.
If the filter field is also limited to choices this also will be included.
X-choices may have two different forms.
The first one when it’s just a representation of choices enum.
Then it’ll be a mapping:
{
    "x-choices": {
        "choices": {
            "value1": "displayValue1",
            "value2": "displayValue2",
            "value3": "displayValue3",
            "value4": "displayValue4",
        }
    }
}
This is simply a mapping of values to display values.
This may be useful during displaying choices in for example drop-down.
The second form of x-choices is:
{
    "x-choices": {
        "operationRef": "#/paths/manufacturer_viewset~1",
        "value": "$response.body#results/*/id",
        "display": "$response.body#results/*/name"
    }
}
This x-choices is a reference to a different endpoint.
This may be used to read limited choices from the related endpoint.
* operationRef - is a JSON pointer to the related endpoint which should be accessible in this chema
* value - shows which field should be taken as a field value
* display - shows which field should be taken as field display value