Skip to content

# Class Based Operations

This is just a proposal and it is not present in library code, but eventually this can be a part of Django Ninja.

Please consider adding likes/dislikes or comments in github issue to express your feeling about this proposal

## Problem

An API operation is a callable which takes a request and parameters and returns a response, but it is often a case in real world when you need to reuse the same pieces of code in multiple operations.

Let's take the following example:

- we have a Todo application with Projects and Tasks
- each project has multiple tasks
- each project may also have an owner (user)
- users should not be able to access projects they do not own

Model structure is something like this:

    class Project(models.Model):
        title = models.CharField(max_length=100)
        owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)

    class Task(models.Model):
        project = models.ForeignKey(Project, on_delete=models.CASCADE)
        title = models.CharField(max_length=100)
        completed = models.BooleanField()

Now, let's create a few API operations for it:

- a list of tasks for the project
- some task details
- a 'complete task' action

The code should validate that a user can only access his/her own project's tasks (otherwise, return 404)

It can be something like this:

    router = Router()

    @router.get('/project/{project_id}/tasks/', response=List[TaskOut])
    def task_list(request):
        user_projects = request.user.project_set
        project = get_object_or_404(user_projects, id=project_id))
        return project.task_set.all()

    @router.get('/project/{project_id}/tasks/{task_id}/', response=TaskOut)
    def details(request, task_id: int):
        user_projects = request.user.project_set
        project = get_object_or_404(user_projects, id=project_id))
        user_tasks = project.task_set.all()
        return get_object_or_404(user_tasks, id=task_id)

    @router.post('/project/{project_id}/tasks/{task_id}/complete', response=TaskOut)
    def complete(request, task_id: int):
        user_projects = request.user.project_set
        project = get_object_or_404(user_projects, id=project_id))
        user_tasks = project.task_set.all()
        task = get_object_or_404(user_tasks, id=task_id)
        task.completed = True
        task.save()
        return task

As you can see, these lines are getting repeated pretty often to check permission:

    user_projects = request.user.project_set
    project = get_object_or_404(user_projects, id=project_id))

You can extract it to a function, but it will just make it 3 lines smaller, and it will still be pretty polluted ...

## Solution

The proposal is to have alternative called "Class Based Operation" where you can decorate the entire class with a `path` decorator:

    from ninja import Router

    router = Router()

    @router.path('/project/{project_id}/tasks')
    class Tasks:
        def __init__(self, request, project_id=int):
            user_projects = request.user.project_set
            self.project = get_object_or_404(user_projects, id=project_id))
            self.tasks = self.project.task_set.all()

        @router.get('/', response=List[TaskOut])
        def task_list(self, request):
            return self.tasks

        @router.get('/{task_id}/', response=TaskOut)
        def details(self, request, task_id: int):
            return get_object_or_404(self.tasks, id=task_id)

        @router.post('/{task_id}/complete', response=TaskOut)
        def complete(self, request, task_id: int):
            task = get_object_or_404(self.tasks, id=task_id)
            task.completed = True
            task.save()
            return task

All common initiation and permission checks are placed in the constructor:

    @router.path('/project/{project_id}/tasks')
    class Tasks:
        def __init__(self, request, project_id=int):
            user_projects = request.user.project_set
            self.project = get_object_or_404(user_projects, id=project_id))
            self.tasks = self.project.task_set.all()

This makes the main business operation focus only on tasks (exposed as the `self.tasks` attribute)

You can use both `api` and `router` instances to support class paths.

## Issue

The `__init__` method:

`def __init__(self, request, project_id=int):`

Python doesn't support the `async` keyword for `__init__`, so to support async operations we need some other method for initialization, but `__init__` sounds the most logical.

## Your thoughts/proposals

Please give you thoughts/likes/dislikes about this proposal in the github issue