Skip to content

feat(mixins): add mixins #279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions docs/api_controller/model_controller/05_mixins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Programmatic Controller Mixins

## Core Components

- **`MixinModelControllerBase`**: The required base class for controllers using this system. It manages route creation and model configuration.
- **Endpoint Mixins**: Declarative classes that add specific endpoints (e.g., list, create) to the controller.

## Quickstart: Full CRUD API

To create all CRUD endpoints for a Django model, inherit from `MixinModelControllerBase` and `CRUDModelMixin`.

**Model:**
```python
from django.db import models

class Project(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
```

**Controller:**
```python
from ninja_extra import api_controller
from ninja_extra.mixins import MixinModelControllerBase, CRUDModelMixin
from .models import Project

@api_controller("/projects")
class ProjectController(MixinModelControllerBase, CRUDModelMixin):
model_class = Project
```
This automatically creates the following endpoints:
- `POST /projects/`
- `GET /projects/`
- `GET /projects/{id}/`
- `PUT /projects/{id}/`
- `PATCH /projects/{id}/`
- `DELETE /projects/{id}/`

## Available Mixins

### CRUD Mixins

| Mixin | HTTP Method | Path | Description |
|:---------------------|:------------|:---------|:-----------------------------------|
| `ListModelMixin` | `GET` | `/` | List all model instances. |
| `CreateModelMixin` | `POST` | `/` | Create a new model instance. |
| `RetrieveModelMixin` | `GET` | `/{id}/` | Retrieve a single model instance. |
| `PutModelMixin` | `PUT` | `/{id}/` | Fully update a model instance. |
| `PatchModelMixin` | `PATCH` | `/{id}/` | Partially update a model instance. |
| `DeleteModelMixin` | `DELETE` | `/{id}/` | Delete a model instance. |

### Convenience Mixins

| Mixin | Bundles | Purpose |
|:-------------------|:---------------------------------------|:-----------------------------------|
| `ReadModelMixin` | `ListModelMixin`, `RetrieveModelMixin` | Read-only endpoints. |
| `UpdateModelMixin` | `PutModelMixin`, `PatchModelMixin` | Write-only update endpoints. |
| `CRUDModelMixin` | All mixins | Full Create, Read, Update, Delete. |

## Controller Configuration

Override class variables on your controller to customize behavior.

### `model_class` (Required)
The Django model to be exposed via the API.
```python
class MyController(MixinModelControllerBase, ReadModelMixin):
model_class = MyModel
```

### `input_schema` & `output_schema`
Override the auto-generated Pydantic schemas. `input_schema` is used for `POST`/`PUT`/`PATCH` bodies. `output_schema` is used for all responses.
```python
class MyController(MixinModelControllerBase, CRUDModelMixin):
model_class = MyModel
input_schema = MyCustomInputSchema
output_schema = MyCustomOutputSchema
```

### `lookup_field`
Customize the URL parameter for single-object lookups. Defaults to the model's primary key (e.g., `{int:id}`).
```python
class MyController(MixinModelControllerBase, ReadModelMixin):
model_class = MyModel
# Generates path: /my-model/{str:name}/
lookup_field = "{str:name}"
```

### `ordering_fields`
Enable and constrain query parameter ordering on list endpoints.
```python
class MyController(MixinModelControllerBase, ReadModelMixin):
model_class = MyModel
ordering_fields = ["name", "created_at"]
```
Enables requests like `GET /?ordering=name` and `GET /?ordering=-created_at`.

### `auto_operation_ids` & `operation_id_prefix`
Control OpenAPI `operationId` generation.
```python
class MyController(MixinModelControllerBase, ReadModelMixin):
model_class = MyModel

# Option 1: Disable completely
auto_operation_ids = False

# Option 2: Add a custom prefix
# Generates: "listCustomPrefix", "getCustomPrefix"
operation_id_prefix = "CustomPrefix"
```

## API for Django Choices

If `model_class` is set to a Django `Choices` enum, `ListModelMixin` creates a read-only endpoint that lists the available choices.

**Model**

```python
from django.db import models

class TaskStatus(models.TextChoices):
PENDING = "PENDING", "Pending"
COMPLETED = "COMPLETED", "Completed"
```

**Controller**
```python
from ninja_extra.mixins import MixinModelControllerBase, ReadModelMixin
from .models import TaskStatus

@api_controller("/task-statuses")
class TaskStatusController(MixinModelControllerBase, ReadModelMixin):
model_class = TaskStatus
```
A `GET /task-statuses/` request returns a (paginated) list of choices:
```json
[
{
"id": "COMPLETED",
"label": "Completed"
},
{
"id": "PENDING",
"label": "Pending"
}
]
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ nav:
- Model Configuration: api_controller/model_controller/02_model_configuration.md
- Model Service: api_controller/model_controller/03_model_service.md
- Parameters: api_controller/model_controller/04_parameters.md
- Mixins: api_controller/model_controller/05_mixins.md
- Usage:
- Quick Tutorial: tutorial/index.md
- Authentication: tutorial/authentication.md
Expand Down
4 changes: 2 additions & 2 deletions ninja_extra/controllers/model/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ def get_one(self, pk: t.Any, **kwargs: t.Any) -> t.Any:
async def get_one_async(self, pk: t.Any, **kwargs: t.Any) -> t.Any:
return await sync_to_async(self.get_one, thread_sensitive=True)(pk, **kwargs)

def get_all(self, **kwargs: t.Any) -> t.Union[QuerySet, t.List[t.Any]]:
def get_all(self, **kwargs: t.Any) -> QuerySet:
return self.model.objects.all()

async def get_all_async(self, **kwargs: t.Any) -> t.Union[QuerySet, t.List[t.Any]]:
async def get_all_async(self, **kwargs: t.Any) -> QuerySet:
return await sync_to_async(self.get_all, thread_sensitive=True)(**kwargs)

def create(self, schema: PydanticModel, **kwargs: t.Any) -> t.Any:
Expand Down
Loading