About organizing code in django applications or thick models is fine
- Transfer
- Those who have not thought about such issues
- Those who already have their own views on the organization of logic, but do not mind evaluating alternatives
- For those who are already using the discussed approach, to confirm their thoughts
- Those who no longer use the discussed approach and have arguments against
There won't be much code, the article is mostly debatable. Angoy)

Thick models.
Intro
Most of the django tutorials and examples on this framework set a bad example for beginners in terms of organizing code in their projects. In the case of a large / long-running application, the code structure, drawn from such examples, can cause non-trivial problems and difficult situations during the development process. In this article, I will describe an alternative, rarely encountered approach to organizing code, which, I hope, will seem interesting to you.
MVC in django = MTV + embedded C
Have you ever tried to explain how MTV works in django, for example, to a RoR developer? Someone might think that templates are views, and views are controllers. Not certainly in that way. A controller is a django-built URL router that provides request-response logic. Representations are needed to represent the right data in the right patterns. Templates and presentations together constitute a “presentation” layer of the framework.
There are many advantages to such MTV - with its help, you can easily and quickly create typical and not only applications. However, it remains unclear where the logic for processing and editing data should be stored, where to abstract the code in which cases. Let's evaluate several different approaches and look at the results of their application.
Logic in Views
Put all or most of the logic in the view. The approach most commonly found in various tutorials and for beginners. It looks something like this:
def accept_quote(request, quote_id, template_name="accept-quote.html"):
quote = Quote.objects.get(id=quote_id)
form = AcceptQuoteForm()
if request.METHOD == 'POST':
form = AcceptQuoteForm(request.POST)
if form.is_valid():
quote.accepted = True
quote.commission_paid = False
# назначаем комиссию
provider_credit_card = CreditCard.objects.get(user=quote.provider)
braintree_result = braintree.Transaction.sale({
'customer_id': provider_credit_card.token,
'amount': quote.commission_amount,
})
if braintree_result.is_success:
quote.commission_paid = True
transaction = Transaction(card=provider_credit_card,
trans_id = result.transaction.id)
transaction.save()
quote.transaction = transaction
elif result.transaction:
# обрабатываем ошибку, позже таск будет передан в celery
logger.error(result.message)
else:
# обрабатываем ошибку, позже таск будет передан в celery
logger.error('; '.join(result.errors.deep_errors))
quote.save()
return redirect('accept-quote-success-page')
data = {
'quote': quote,
'form': form,
}
return render(request, template_name, data)
It is extremely simple, therefore attractive at first glance - all the code is in one place, you do not need to strain your brain and abstract something there. But this is only at first glance. This approach does not scale well and quickly leads to a loss of readability. I had the good fortune to see the performance on five hundred lines of code, from which even experienced developers drove cheekbones and clenched their fists. Thick views lead to duplication and complexity of the code, it is difficult to test and debug them and, as a result, is easy to break.
Logic in forms
Forms in django are object-oriented, they validate and clean up data, which is why they can also be considered as a place for logic.
def accept_quote(request, quote_id, template_name="accept-quote.html"):
quote = Quote.objects.get(id=quote_id)
form = AcceptQuoteForm()
if request.METHOD == 'POST':
form = AcceptQuoteForm(request.POST)
if form.is_valid():
# инкапсулируем логику в форме
form.accept_quote()
success = form.charge_commission()
return redirect('accept-quote-success-page')
data = {
'quote': quote,
'form': form,
}
return render(request, template_name, data)
Already better. The problem is that now the payment acceptance form is also processing credit card fees. Necomilfo. What if we want to use this function in some other place? We, of course, are smart and could encode the necessary impurities, but again, what if we need this logic in the console, in celery or another external application? The decision to instantiate a form to work with the model does not look right.
Code in Class Based Views
The approach is very similar to the previous one - the same advantages, the same disadvantages. We do not have access to logic from the console or from external applications. Moreover, the scheme of view inheritance in the project is complicated.
utils.py
Another simple and tempting approach is to abstract all sub-code from the submissions and place it as utility functions in a separate file. It would seem that a quick solution to all problems (which many eventually choose), but let's think a little.
def accept_quote(request, quote_id, template_name="accept-quote.html"):
quote = Quote.objects.get(id=quote_id)
form = AcceptQuoteForm()
if request.METHOD == 'POST':
form = AcceptQuoteForm(request.POST)
if form.is_valid():
# инкапсулируем логику в utility-функции
accept_quote_and_charge(quote)
return redirect('accept-quote-success-page')
data = {
'quote': quote,
'form': form,
}
return render(request, template_name, data)
The first problem is that the location of such functions may not be obvious, the code related to certain models may be spread over several packages. The second problem is that when your project grows and new / other people start working on it, it will not be clear which functions exist at all. Which, in turn, increases development time, annoys developers and can cause code duplication. Certain things can be caught on a code review, but this is a post factum solution.
Solution: fat models and fat managers
Models and their managers are an ideal place to encapsulate code intended for processing and updating data, especially when such code is logically or functionally tied to the capabilities of ORM. In fact, we extend the model API with our own methods.
def accept_quote(request, quote_id, template_name="accept-quote.html"):
quote = Quote.objects.get(id=quote_id)
form = AcceptQuoteForm()
if request.METHOD == 'POST':
form = AcceptQuoteForm(request.POST)
if form.is_valid():
# инкапсулируем логику в методе модели
quote.accept()
return redirect('accept-quote-success-page')
data = {
'quote': quote,
'form': form,
}
return render(request, template_name, data)
For my taste, such a solution is the most correct. The code for processing credit cards is elegantly encapsulated, the logic is in a place relevant to it, the necessary functionality is easy to find and (re) use.
Summary: general algorithm
Where to write the code
- Code in Model Method
- Code in the manager method
- Code in the form method
- Code in CBV Method
If none of the options fit, it might be worth considering abstracting into a separate utility function.
TL; DR
The logic in the models improves django applications,
Bonus
In the comments to the original topic slipped links to two interesting applications that are close to the topic of the article.
github.com/kmmbvnr/django-fsm - state machine support for django models (from the description). Set the FSMField field on the model and track the change in predefined states using the decorator in the spirit of receiver .
github.com/adamhaney/django-ondelta - an admixture for django-models that allows you to handle changes in the fields of the model. Provides an API in the style of its own clean _ * - model methods . Does exactly what the description says.
There, another approach was proposed - to abstract all the code related to business logic into a separate module. For example, in the prices application, we select all the code responsible for processing prices in the processing module. Similar to the utils.py approach, it differs in that we abstract the business logic, and not everything in a row.
In my own projects, I generally use the approach of the author of the article, adhering to the following logic:
- Code in the model method - if the code refers to a specific instance of the model
- Code in the manager method - if the code affects the entire relevant table
- Code in the form method - if the code validates and / or preprocesses the data from the request
- The code in the CBV method is what is relevant to request and is a residual
- In utils.py - code not directly related to the project
Will we discuss it?