HackSoftware / Django-Styleguide

Django styleguide used in HackSoft projects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to handle server rendered forms

danlamanna opened this issue · comments

Thanks for the styleguide. I've noticed that it seems particularly targeted at API servers where a separate SPA is built. I'm interested in the use case of django form validation.

How would the notion of a custom field validator in a django form translate to services?

@danlamanna Hello 👋

If you treat forms as serializers, the rest of it is pretty much the same.

Here's an example from a few days ago, taken from a custom admin:

from django import forms

from django.contrib import admin, messages
from django.core.exceptions import ValidationError

from styleguide_example.files.models import File
from styleguide_example.files.services import (
    FileStandardUploadService
)


class FileForm(forms.ModelForm):
    class Meta:
        model = File
        fields = ["file", "uploaded_by"]


@admin.register(File)
class FileAdmin(admin.ModelAdmin):
    list_display = [
        "id",
        "original_file_name",
        "file_name",
        "file_type",
        "url",
        "uploaded_by",
        "created_at",
        "upload_finished_at",
        "is_valid",
    ]
    list_select_related = ["uploaded_by"]

    ordering = ["-created_at"]

    def get_form(self, request, obj=None, **kwargs):
        """
        That's a bit of a hack
        Dynamically change self.form, before delegating to the actual ModelAdmin.get_form
        Proper kwargs are form, fields, exclude, formfield_callback
        """
        if obj is None:
            self.form = FileForm

        return super().get_form(request, obj, **kwargs)

    def get_readonly_fields(self, request, obj=None):
        """
        We want to show those fields only when we have an existing object.
        """

        if obj is not None:
            return [
                "original_file_name",
                "file_name",
                "file_type",
                "created_at",
                "updated_at",
                "upload_finished_at"
            ]

        return []

    def save_model(self, request, obj, form, change):
        try:
            cleaned_data = form.cleaned_data

            service = FileStandardUploadService(
                file_obj=cleaned_data["file"],
                user=cleaned_data["uploaded_by"]
            )

            if change:
                service.update(file=obj)
            else:
                service.create()
        except ValidationError as exc:
            self.message_user(request, str(exc), messages.ERROR)

As you can see, we are using form.cleaned_data to pass data to the service.

You'll basically do the same, but within a view.

And if there are errors, you can always use https://docs.djangoproject.com/en/4.0/ref/forms/api/#django.forms.Form.add_error to bring the error back to the form & render properly.

Basically:

  1. Use forms as serializers
  2. Don't save() via forms
  3. Attach errors back

Let me know if this explanation is good enough 👍

I think this example helps illustrate my exact concern!

Here, you're setting a generic validation error for something like https://github.com/HackSoftware/Django-Styleguide-Example/blob/c01431d032f2a8c0e4d915bdf952ce07cba20c22/styleguide_example/files/services.py#L28. IMO, this error should be attached to the file field.

Perhaps this is a more fundamental question with services. How should per field errors be raised in such a way that the field validation logic is deduplicated between API views (serializers) and SSR views (forms)?

@danlamanna This is a good question.

There are a couple of things that we need to look at.

First of all, I try to differentiate the kinds of validation that we can have:

  1. Validate the data, that's coming, if it follows the proper format & rules.
  2. Validate the data, that's coming, in the context of the application (that's basically business logic)

For the first kind of validation, both forms & serializers are doing a great job. You can add custom fields, you can write a complex cleaning logic and it will eventually result in a ValidationError, that you app should know how to handle.

For those kind of validation, my personal preference is to stick to basic Form and Serializer & not ModelForm and ModelSerializer. Once you put a model in the mix, you go to the 2nd type of validation.

For the 2nd type of validation, there are options:

  1. You extend your model to do that ( https://github.com/HackSoftware/Django-Styleguide#validation---clean-and-full_clean / https://github.com/HackSoftware/Django-Styleguide#validation---constraints) - this relies on someone calling the .full_clean() method.
  2. You can write your actual business-layer validation logic in your service layer (and combine that with calling .full_clean())

If you take this approach, your app will have the following flow:

Random

So you won't have to dedup anything from serializers & forms.

The extra steps that you need to take care is:

  1. Returning errors back to the form, so you can render that. This can be handled by the API with a nice little utility - taking exceptions & putting them back in the form
  2. Deciding how your API errors are going to look like. We've written a few lines about that here - https://github.com/HackSoftware/Django-Styleguide#errors--exception-handling

Cheers!

@danlamanna Freel free to reopen this if you have additional questions 🙌