# Django REST framework extension

## Serializers

### BaseModelSerializer

This serializers extends the restframework ``ModelSerializer`` but ensures that the ``clean()`` method in the model is
called. By default, only django forms call the model-level clean, not the serializers.

````
# models.py
class MyModel(models.Model):
    ...

    def clean():
        # here lives the model-level validation
        ...

# serializers.py
class MyModelSerializer(ModelSerializer)
    class Meta:
        model = MyModel

    # do your stuff here!
````

Just derive your serializer from the ``BaseModelSerializer`` and you are good to go!

### CommonInfoSerializer

In addition to the ``CommonInfo`` model class which provides a neat way to set the creator and last editor of any
database object, this serializers takes care of setting those fields if a request user was found.

Furthermore, it extends the ``BaseModelSerializer`` to ensure that model-level validation is called on serializer
validation.

````
# models.py
class MyOwnershipRelevantModel(CommonInfo):
    ...

# serializers.py
class MyOwnershipRelevantModelSerializer(CommonInfoSerializer)
    class Meta:
        model = MyOwnershipRelevantModel

    # do your stuff here!
````

## Fields

### RecursiveField

In some cases you'll have to define a foreign key from and to the same model. One popular use-case is a simple tree
structure. If you want to use the same serializer for the parent and all children, you can utilise our `RecursiveField`:

````
class MyModelSerializer(serializers.ModelSerializer):
    ...
    children = RecursiveField(many=True)

    class Meta:
        model = MyModel
        fields = [
            'id',
            ...
            'children',
        ]
````

## Testing of ViewSets

If you want to test `ViewSets` you can use the handy mixin `BaseViewSetTestMixin` from this package. It encapsulates
common use-cases and provides a wrapper to be able to easily focus on your tests and not the syntax of how to call a
viewset manually.

### Setup

It is easy to use this mixin, just follow these steps. You will probably have some base test class to encapsulate your
general test setup. So create a new base api test class like this:

````
class BaseApiTest(BaseViewSetTestMixin, BaseTest):
    def get_default_api_user(self) -> AbstractUser:
        return baker.make_recipe('apps.account.tests.user')

````

Note that the content of `get_default_api_user()` is just a suggestion. You have to provide the method, but it is
totally up to you how you generate a base request user.

The idea behind this method is, that you define a base user for your API requests. You still have to pass the user
manually to all of your tests in the `execute_request()` method. We did not set this user as a default because the user
is often an essential part of the test and should be selected consciously and with care.

If you do not need this default user, simply return `None` in the described method.

### Test that authentication is required for an endpoint

Permissions are one of the most vital things to test in your application. So this mixin provides a helper method, so you
are able to test what you require without thinking about how to test it.

````
class MyFancyViewsetApiTest(BaseApiTest):
    def test_list_authentication_required(self):
        self.validate_authentication_required(
            url=reverse('my-fancy-viewset-list'),
            method='get',
            view='list',
        )
````

### Testing CRUD views and custom actions

Here is an example of how to test a CRUD action. The helper method `execute_request()` encapsulates all necessary logic
to create an API response. This response can then be asserted. It is wise to always at first assert the status code. In
this case, if the API does not return valid data, but an error code, the reason of failure is obvious. If you on the
other hand start with asserting response data, you might get an ``IndexError`` which will point the assigned developer
in the wrong direction at first.

````
class MyFancyViewsetApiTest(BaseApiTest):
    def test_list_regular(self):
        response = self.execute_request(
            method='get',
            url=reverse('my-fancy-viewset-list'),
            viewset_kwargs={'get': 'list'},
            user=self.user,
        )

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 3)
````

Notice that you need to tell the viewset which CRUD view you want to test. Here is a list of valid default parameters
you can pass to `viewset_kwargs`:

| Name   | Key    | Value    |
|--------|--------|----------|
| List   | get    | list     |
| Detail | get    | retrieve |
| Create | post   | create   |
| Update | put    | update   |
| Delete | delete | destroy  |

The "Key" is the HTTP method to be used, and the value is the name of the action. If you have a **custom action**, just
choose the corresponding method and pass the action name as "value".

### Validate that a CRUD method is deactivated

It might be vital for your application that some CRUD (create, retrieve, update, delete) views are exposed with a given
viewset but others are not. It happens very easily that a generic DRF mixin is copy-and-pasted and opens an unwanted
`list`, `retrieve`, `create`, etc. endpoint. That is why we recommend testing if all unwanted CRUD methods are
deactivated.

With this mixin you can ensure this easily like this:

````
class MyFancyViewsetApiTest(BaseApiTest):
    view_class = views.MyFancyViewset
    ...

    def test_create_not_activated(self):
        with self.assertRaises(AttributeError):
            self.execute_request(
                method='post',
                url=reverse('my-fancy-viewset-list'),
                viewset_kwargs={'post': 'create'},
                user=self.user,
            )
````

### Testing file uploads

Here is an example of how to test file uploads. Feel free to adjust the `data` kwarg to suit your needs.

````
from django.core.files.uploadedfile import SimpleUploadedFile

class MyFancyViewsetApiTest(BaseApiTest):
    view_class = views.MyFancyViewset
    ...

    def test_file_upload_action(self):
        response = self.execute_request(
            method='post',
            url=reverse('my-fancy-viewset-list'),
            viewset_kwargs={'post': 'my-custom-action'},
            user=self.user,
            data={'file': SimpleUploadedFile("file.txt", b"some fancy content")},
            data_format='multipart',
        )

        self.assertEqual(response.status_code, status.HTTP_200_OK)
````
