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:
- Use forms as serializers
- Don't
save()
via forms - 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:
- Validate the data, that's coming, if it follows the proper format & rules.
- 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:
- 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. - 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:
So you won't have to dedup anything from serializers & forms.
The extra steps that you need to take care is:
- 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
- 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 🙌