Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where does business logic in a model method go in Django Rest Framework?

I have 3 models that are related to each other with one-to-many relationships.

  • Model A can have many instances of Model B.
  • Model A can have many instances of Model C.
  • Model B can have many instances of Model C.

The idea is that a user would create an instance of Model A (like a stock portfolio) and then enter stock holdings (Model C). Where Model B fits in is that I want to run calculations/logic based on the stocks (Model C) in the portfolio (Model A) and using another class/model to keep track of things makes life easier, hence Model B.

I originally had the logic for these calculations in a Django view but read in Two Scoops of Django that business logic should be separated from views. As a result I moved the logic to a method of Model A (the portfolio) and now call that method from a view. This logic loops through the stock holdings and creates new instances of Model B, the results.

I am now interested in exploring django-rest-framework to provide an API to a javascript frontend (like Angular). I am guessing that I will not be able to have this kind of data manipulation within my REST interfaces. However, the result of this logic (data in Model B) needs to be visible via REST. Therefore, where does this type of calculation/logic go?

like image 263
shudoh Avatar asked Feb 19 '16 14:02

shudoh


1 Answers

Django Rest Framework main pieces are views (ViewSets, ApiViews, etc) and serializers. Neither of those are ideal places to write logic. As you mentioned, writing logic in any view is not good. Why?

  1. No way to use the same logic from another part of the app
  2. No way to encapsulate the logic (you need to call the view for the logic to run)
  3. No way to unit test the logic because is coupled with the view

IMHO, models are not a good place to write logic. Think of a model as your database definition. I would keep them as simple as possible. You can override "save" and other methods to do trivial tasks. Any other advanced functionality should live outside of it.

I can think of two better places for what you need:

One of them is a django signal

A better one is a custom class. Encapsulate/decouple the logic in your own class (you can use static or instance methods, it doesn´t matter), and then you will be able to:

  1. Unit test those methods / mock that class
  2. Reuse this logic somewhere else
  3. Simplify your code according to KISS principle
  4. Use it from a signal keeping the signal code simple
  5. Use it from a celery task (if you implement this in the future) without modifying a single line of your code

UPDATE An example of how to organize the code.

From the comments it´s clear that a signal wouldn´t work because the analysis operation will run on user request. A signal would be useful if that operation should run automatically when saving an specific model.

I´m assuming you know how to use django-rest-framework api views or viewsets, serializers, etc. I you don´t how about that, better ask another question. This is going to be more a python explanation than anything else

In your app module, create a file app_business_logic.pyor whatever you want to call it. You can place it at the same level of the models.py for instance, but it´s not mandatory:

class HoldingsAnalyser:
    # static method sample. Call it like this: "HoldingsAnalyser.run(..)"
    @staticmethod
    def run(holding_list):
        # do your model generation here or whatever you need
        return True # or whatever you need to return

    # instance method sample. Create an instance first and then call the method: 
    # analyser = HoldingsAnalyser()
    # analyser.run(...)
    def run(self, holding_list):
        # do your model generation here or whatever you need
        return True # or whatever you need to return

Now, in a django-rest-framework api view or viewset, create a POST method to be called when the client app user press the button (that button to generate analysis):

from yourapp.app_business_logic import HoldingsAnalyser

class StockPortfolioViewSet(WhatEverMixingYouNeedToInheritFrom):
    serializer_class = whatever # look at the docs
    
    @detail_route(methods=['post'])
    def run_analysis(self, request, pk):
        stock_portfolio = get the object based on the pk
        holdings = make your query to get the holdings
        analysis_result = HoldingsAnalyser.run(holdings)
        if analysis_result:
            # everything ok
            return Response(status=status.HTTP_204_NO_CONTENT)
        else:
            return Response(a useful error for your client)
         

The url for this would be something like http://server.com/api_path/stockportfolio/21/run_analysis/, where 21 would be the id of the StockPortfolio

like image 135
xleon Avatar answered Sep 21 '22 17:09

xleon