Hi! I’m working on a Django project and aiming to adhere to best practices, such as avoiding the service layer and keeping views thin .
I’m using class-based views for creating and updating objects, specifically by overriding the form_valid
method to add business logic. Here’s an example:
class PetCreateView(CreateView):
model = Pet
fields = ['name', 'age', 'animal_type']
template_name = 'pets/pet_create.html'
success_url = reverse_lazy('pets')
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.owner = self.request.user
self.object.clean() # Question 2
self.object.save()
return redirect(self.get_success_url())
Question 1:
Instead of saving with commit=False
, I could directly assign the owner in form.instance
like this:
def form_valid(self, form):
form.instance.owner = self.request.user
form.instance.clean() # Question 2
return super().form_valid(form)
The Pet
instance is then saved by the parent form_valid
method. Would this be a better approach?
Question 2:
If I have validation logic inside the Pet
model related to the user, I could call clean()
in the view, and check if the Pet
instance has an owner before validating it, to avoid an AttributeError
in the initial clean
call:
class Pet(models.Model):
def clean(self):
super().clean()
if self._state.adding:
self._validate_owner_can_adopt_pet()
def _validate_owner_can_adopt_pet(self):
if hasattr(self, 'owner') and not self.owner.can_adopt():
raise ValidationError(_('This owner cannot adopt more pets.'))
Alternatively, I could skip the clean()
call in the view and add the owner before the initial clean
like this:
class PetCreateView(CreateView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if kwargs['instance'] is None:
kwargs['instance'] = self.model()
kwargs['instance'].owner = self.request.user
return kwargs
Which approach would you recommend?
Question 3:
If the logic becomes more complex, for example:
class TeamCreateView(CreateView):
model = Team
fields = ['name', 'description']
template_name = 'team/team_create.html'
success_url = reverse_lazy('team')
def form_valid(self, form):
user: User = self.request.user
with transaction.atomic():
self.object = form.save()
member = self.object.add_member(user)
self.object.set_admin(member)
return redirect(self.get_success_url())
Would it be better to move this logic into a custom manager? For example:
class TeamManager(models.Manager['Team']):
@transaction.atomic
def create_and_set_admin_member(self, user: 'User', **kwargs) -> 'Team':
team = self.create(**kwargs)
member = team.add_member(user)
team.set_admin(member)
return team
Then, in the view:
def form_valid(self, form):
user: User = self.request.user
self.object = Employer.objects.create_and_set_admin_member(user=user, **form.cleaned_data)
return redirect(self.get_success_url())
Although I like the idea of separating the logic from the view, I’m unsure whether this is the right approach. The ModelForm.save()
method has additional logic for many-to-many fields and error handling, and I’m not sure if bypassing it by using a custom manager is the best practice. Additionally, this creates a new instance from scratch, which seems wasteful, though I could pass the instance to the manager method to avoid this.
Question 4:
Since FormView
doesn’t call form.save()
in form_valid()
, how would you recommend handling business logic in this case, compared to using create/update views?
Thank you!