models_serializers_viewsets.md 9.13 KB
Newer Older
1 2 3 4 5 6
Models, Serializers and ViewSets
=============

## Basics

Our backend app is based on Django. If you are familiar with Django then you will understand what is a Model.
7
For this project, the general idea is that Django is used to handle the data and nothing else. Lets say that we are only using the "M" of "MVC" of a standard Django architecture.
8 9 10 11 12 13 14 15

To be able to transfer data to the frontend, we use the famous Django extension [django-rest-framework](https://www.django-rest-framework.org/).

?> Make sure to look at its documentation and code if you want to now more about it.


To put it into a nutshell, we have:

16 17 18 19 20 21 22 23 24
- **Models** that handle all data and the exchanges with the database,
- **Serializers** that handle the conversion from models' data to JSON and from JSON to models' data,
- **ViewSets** that handle communication with the frontend.


!> :warning: **Before moving to the details of the models/serializers/viewsets you must now that a lot is automated in the REX-DRI backend. Especially regarding *importing* those classes in Django admin and registering the viewsets with their configuration. All is centralized in *config files* (make sure to read the [doc about them](Application/Backend/config_files.md): if you add a Model/Viewset you WILL need to *register* them in those config files for them to be imported at runtime.** :warning:

*NB: as it is explained in the previously cited [documentation page](Application/Backend/config_files.md), models are registered "through" the associated viewset.*

25 26 27 28 29 30 31 32 33

## In details

Since the process of creating the API was a bit repetitive, we have used a lot of [inheritance](Application/Backend/architecture.md) and config files to help managing the code base.

Below are the things to know about what has been implemented.

### Models

34 35 36
#### App specific

There exist different *abstract* models from which the *concrete* models can inherit from. Those *abstract* models have different behaviors and properties built in directly.
37 38 39 40 41

- `MyModel`: contains attribute that should be shared by all models in the app such as for storing who updating something, etc.
- `MyModelVersionned`: inherits from `MyModel` and make it possible to version the data contain in the models that inherit from it.
- `BasicModule`: inherits from `MyModelVersionned` and provides a set of attributes such a title, a comment, a custom useful links field and an importance level.

42
?> :warning: what is said below will soon be unnecessary: https://gitlab.utc.fr/rex-dri/rex-dri/issues/85
43

44
Models that inherit from `MyModelVersionned` (and therefore `BasicModule`) must have the following class method that returns the associated serializer:
45 46 47 48 49 50 51 52 53 54

```python
class MyAwesomeClass(MyModelVersionned):
    @classmethod
    def get_serializer(cls):
        return MyAwesomeClassSerializer
    ...
```

This method is used in the `VersionSerializer` class to convert version data back to a model instance.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203

#### General idea

So to define a model for the app, you should (in most cases) use one of the three base classes defined above. What remains is to define a model just like you would do for any Django Model. Here is an example (you have tons of them in app already):

```python
from django.db import models
from backend_app.models.abstract.my_model import MyModel

class Country(MyModel):
    name = models.CharField(max_length=200)
    iso_alpha2_code = models.CharField(primary_key=True, max_length=2)
    iso_alpha3_code = models.CharField(
        unique=True, max_length=3, default="", blank=True
    )
    region_name = models.CharField(max_length=200)
    region_un_code = models.CharField(max_length=3)
    sub_region_name = models.CharField(max_length=200, default="", blank=True)
    sub_region_un_code = models.CharField(max_length=3, default="", blank=True)
    intermediate_region_name = models.CharField(max_length=200, default="", blank=True)
    intermediate_region_un_code = models.CharField(max_length=3, default="", blank=True)
```

### Serializers

#### App specific

To go along the previously mentioned *abstract* models, we have matching serializers that handle all the logic behind rendering the models' field to JSON, validating the data before creating/updating a model instance and handling the specific behaviors (such as moderation — that comes with all models that inherit from `MyModel` — and versioning — that comes with all models that inherit from `MyModelVersionned`).

:warning: **95% of the *magic* behind those not that simple behaviors are handled in a subtle, sometimes *hacky*, manner.**

:information_desk_person: All those specific behaviors should have been correctly unit-tested at this point.

?> You can find more information about moderation and versioning [here](Application/Backend/moderation_and_versioning.md).


#### General idea

To create a serializer for your model, you need to create a class that inherits from the matching parent class of your model. For instance the `Country` model inherits from the `MyModel` *abstract* class, so the `CountrySerializer` inherits from `MyModelSerializer`.

Then you must define a `Meta` class inside the serializer that countains information about what model is the serializer for and what fields (attribute) should be taken into account. For instance for the country model we have the following serializer:

```python
class CountrySerializer(MyModelSerializer):
    class Meta:
        model = Country
        fields = "__all__"
```

So the serialization of all fields will be handled automatically by `django-rest-framework`.

You may also decide to exclude some fields:

```python
class UniversitySerializer(MyModelSerializer):
    class Meta:
        model = University
        exclude = ("utc_id",)
```

And even include custom serializer to make some fields read only, or add data to send to the client:

```python
class MyModelSerializer(MySerializerWithJSON):
    # ...
    # Extract of the definition of the class

    # This field is "smartly" set by the serializer on save
    # and should be considered as readonly.
    updated_by = serializers.CharField(read_only=True)

    # For easier handling on the client side, we force an id field
    # this is useful when a model has a dedicated primary key
    id = serializers.SerializerMethodField()

    def get_id(self, obj: MyModel):
        """
        Serializer for the id field.
        """
        return obj.pk

    class Meta:
        model = MyModel
    # ...
```

As shown above, when using a `custom_attr = serializers.SerializerMethodField()`, you need to define a method in the class that is named `get_custom_attr` and returns whatever you want (as soon as it can be converted to JSON).


?> Django-rest-framework is full a functionalities, and only a subset of them have been presentend here. So feel free to have a look at [the package documentation](https://www.django-rest-framework.org/).


### Viewsets

#### App specific

The custom work performed on the generic viewsets is fairly straightforward, you can have a look at it. Anyway, just like with the serializer, you should use the corresponding viewset when inheriting: `MyModelViewset` or `MyModelVersionnedViewset` or `BasicModuleViewSet`.

The general idea is that a good chunk of the parametrization of the viewsets have been moved away from the class definition to the [config files](Application/Backend/config_files.md).


#### General idea

Most often a wiewset class will look like this, with a simple reference to the associated serializer class:

```python
class MyModelVersionnedViewSet(MyModelViewSet):
    """
    Viewset for the versionned models
    """
    serializer_class = MyModelVersionnedSerializer
```

Sometimes you will also need to filter the data against some attribute found in the client request:

```python
class CountryDriViewSet(BasicModuleViewSet):
    queryset = CountryDri.objects.all()  # pylint: disable=E1101
    serializer_class = CountryDriSerializer

    def get_queryset(self):
        country_id = self.kwargs["country_id"]
        return super().get_queryset().filter(countries__pk=country_id).distinct()
```

Or also prefetch some related attributes for optimization purposes:

```python
# Extract of the class definition
class MyModelViewSet(viewsets.ModelViewSet):
    def get_queryset(self):
        """
        Extended default rest framework behavior
        to prefetch some table and enhance performances
        """
        return self.queryset.prefetch_related("moderated_by", "updated_by")
```

?> If you are familiar with `REST` api, you will surely be familiar with filtering elements directly in the request with a syntaxe such as `url/?username=bob&isOld=true`. This syntax is not yet supported, since we didn't need it up until now.


## Keeping things consistent

!> :warning: To keep things consistent the general idea is that if a model is called `Blabla` than it's serializer class will be named `BlablaSerializer` and the associated viewset class will be named `BlablaViewSet`.

:information_desk_person: Sometimes it will be useful to have different viewsets for the same serializer, for instance if you want an api endpoint that returns the data filtered in a certain way.

!> You should avoid having multiple serializers for the same model since it may brake things related to deserializing previous versions of models.