eat some code

Handling statuses in Django #2

Expose transitions to an API

October 2016 #API  #DRF  #Status  #State machine  #Django 

As discussed in the previous article, django-fsm is a great plugin to handle statuses in Django. Let's use it with an API.

Handling statuses in Django #1

To learn more about states, transitions & django-fsm (Django Final State Machine) please check the first article: Handling statuses in Django #1.

Application example

The same example is used in the first article, feel free to skip this part.

To sum it up, let's imaging we're building a publication platform with 2 levels of approval before publication for articles. The following diagram shows the article statuses and transitions:

Frontend VS Backend responsibilities

One common reason to develop an API in Django is to use it in conjunction with a frontend framework such as EmberJS. In our example, we'll show a button when appropriate on the article page. A button would be "Approve" for example.

Solution 1 - to be avoided

The intuitive way to do this is to write the code in the template: if the article is a draft, let's show an "Approve" button. That seems easy enough.

Now take a minute to look at the above diagram and keep in mind that everyone doesn't have access to all transitions and that there could be several possible transitions; having several transitions means several buttons...

For a draft article, the conditions would be: if you're logged-in as an admin and you're looking at a draft article you should see an "Approve" button (as long as you have sufficient access to give that first approval). And if you have sufficient permission, you should also see a "Publish" button (see the transition at the very top of the above diagram).

Coding this in the templates present two major issues:

  • a lot of if / else statement => code smell
  • duplication with the backend => big-ass code odour

This is a really bad choice.

Solution 2

Ideally, you don't want the frontend to know all the details regarding the transitions. In other words, the frontend shouldn't include any business logic. What you want is true separation of concern:

  • The frontend is responsible for:
    • presenting the transitions in the User Interface
    • handling the user interactions
  • The backend must therefore (via the API):
    • provide the available transitions
    • handle permissions & security issues
    • obviously handle the database

In the API, we would add the list of available transitions in the article details call and add a call to update the status (using one of these transitions).

Let's code it:

For the API, I'm using the famous Django Rest Framework plugin and as you can see, the code is truly straightforward. First let's expose the available transitions for the currently logged-in user:

class ArticleSerializer(serializers.ModelSerializer):

    class Meta:
        fields = (
             …
             'transitions',
        )

    def get_transitions(self, article):
        return dict([
            (transition_name, ...)
            for transition_name in article.get_available_transitions(user=self.context['request'].user)
        ])


class ArticleViewSet(…):
    …
    serializer = ArticleSerializer


For a draft article, an admin would get 2 transitions from the API: "approve_1" and "publish". All we now need is an API endpoint to use these transitions. Let's add: PUT /api-path/articles/xx/update_status:

class ArticleViewSet(…):

    @detail_route(methods=['put'], permission_classes=[IsAuthenticated])
    def update_status(self, request, pk):
        article = get_object_or_404(Article, id=pk)
        transition = request.data['transition']    # e.g. "publish"

        if transition not in task.get_available_transitions(self.user):
            return response.Response(
                data={
                    'message': "Invalid transition"
                },
                status=status.HTTP_403_FORBIDDEN
            )

        getattr(article, transition)()
        article.save()

        return response.Response({'message': "Status updated"}, status.HTTP_200_OK)

Conclusion

Et voilà ! You would obviously do more than this... These code samples are enough to get things working nicely (please comment if you know a good alternative). In a real application, I'd check for example that the user has access to the article at all and I'd provide more information via get_transitions (label, confirmation message, etc.).