django-ninja-crudl

The easiest, quickest and most complete way to publish REST API with Create, Read, Update, Delete and List endpoints from Django models.

https://github.com/nextgencontributions/django-ninja-crudl

Science Score: 44.0%

This score indicates how likely this project is to be science-related based on various indicators:

  • CITATION.cff file
    Found CITATION.cff file
  • codemeta.json file
    Found codemeta.json file
  • .zenodo.json file
    Found .zenodo.json file
  • DOI references
  • Academic publication links
  • Academic email domains
  • Institutional organization owner
  • JOSS paper metadata
  • Scientific vocabulary similarity
    Low similarity (15.3%) to scientific vocabulary

Keywords

api crud crudl django django-ninja django-ninja-api openapi openapi-specification python python3 rest rest-api restapi restful-api
Last synced: 4 months ago · JSON representation ·

Repository

The easiest, quickest and most complete way to publish REST API with Create, Read, Update, Delete and List endpoints from Django models.

Basic Info
  • Host: GitHub
  • Owner: NextGenContributions
  • License: mit
  • Language: Python
  • Default Branch: main
  • Homepage:
  • Size: 2.15 MB
Statistics
  • Stars: 4
  • Watchers: 2
  • Forks: 1
  • Open Issues: 25
  • Releases: 19
Topics
api crud crudl django django-ninja django-ninja-api openapi openapi-specification python python3 rest rest-api restapi restful-api
Created about 1 year ago · Last pushed 4 months ago
Metadata Files
Readme Changelog Contributing Funding License Citation

README.md

Why this package?

To provide the most simplest, quickest and complete way to expose Django models securely as RESTful API CRUDL (Create, Retrieve, Update, Delete, List) endpoints and provide the most complete OpenAPI documentation for those endpoints.

The key objectives of this package, that make it unique and different from other similar packages, are:

  • Handles the model relationships and related objects in the most complete way: This includes the one-to-one, one-to-many and many-to-many relationships, and the reverse relationships of those, etc. during the CRUDL operations.
  • The most complete and accurate OpenAPI documentation for the CRUDL endpoints: This applies to the field types and details, query parameters, error responses etc.
  • Enough flexibility to customize the CRUDL endpoints to meet the most of the use cases: The developer can define exposable fields per operation/endpoint type, the permission checks, pre and post hooks, and additional REST endpoints in the CRUDL controller.

What is it?

The package provides a set of classes and methods to expose Django models via RESTful API CRUDL endpoints.

Behind the scenes, the package uses the Django Ninja Extra package which in turn uses Django Ninja. For generating the input and output validation schemas, django2pydantic package is used.

Currently Python 3.12+, Django 5.1+, Pydantic 2.9 are supported. We are expanding the official support to other versions once the cross version test suite is in place.

Tutorial on how to use

Installation

Until the first release version is published on PyPI, you need to install it directly from the GitHub repository:

With Pip:

bash pip install git+https://github.com/NextGenContributions/django-ninja-crudl.git

With Poetry:

bash poetry add git+https://github.com/NextGenContributions/django-ninja-crudl.git

With uv:

bash uv add git+https://github.com/NextGenContributions/django-ninja-crudl

Define the Django model and the CRUDL controller class along the fields exposed via the CRUDL endpoints

Let's assume you have the following Django models:

```python

models.py:

from django.db import models

class SomeRelatedModel(models.Model): id = models.AutoField(primarykey=True) details = models.CharField(maxlength=255)

class MyModel(models.Model): id = models.AutoField(primarykey=True) name = models.CharField(maxlength=255) description = models.TextField() somerelatedmodel = models.ForeignKey(SomeRelatedModel, on_delete=models.CASCADE) ```

Then you need to define the CRUDL controller for the model using the new config-based approach:

```python

crudl.py:

from djangoninjacrudl import CrudlController, CrudlConfig, Infer, Schema from .models import MyModel

class MyModelCrudl(CrudlController[MyModel]): config = CrudlConfigMyModel, updateschema=SchemaMyModel, getoneschema=SchemaMyModel, listschema=SchemaMyModel, delete_allowed=True, ) ```

NOTE: In order to avoid accidentally exposing sensitive fields, you need to explicitly define the model fields that shall be exposed via the CRUDL endpoints. Some other libraries support exposing all fields (with optional exclude) which can lead to unintentional exposure of sensitive data.

NOTE: If any of create_schema, update_schema, get_one_schema, or list_schema are not defined or are set as None, then that specific endpoint will not be exposed. If delete_allowed is not defined or set as False, then the delete endpoint will not be exposed.

NOTE: For delete operation, it currently performs a hard delete by default. You might customize the delete operation to perform a soft delete by overriding the delete method in the model.

NOTE: The Infer class from the django2pydantic library is used to tell that the field type and other details shall be inferred from the Django model field.

NOTE: As you can see from the above example, the library allows using different schemas for the create, update/partial update, get one, and list operations. For example, this can have the following practical advantages:

  • Allows that some fields can be used during create but cannot be updated after the creation
  • The list operation can expose only the fields that are needed for the list view
  • The get one operation can expose more details, like more details from related objects, than the list operation.

Register the endpoint

Then you need to register the CRUDL controller in the API for Django:

```python

api.py:

from djangoninjacrudl.api import NinjaCrudlAPI from .crudl import MyModelCrudl

api = NinjaCrudlAPI() api.register_controllers(MyModelCrudl)

urls.py:

from django.contrib import admin from django.urls import path from .api import api

urlpatterns = [ path("admin/", admin.site.urls), path("api/", api.urls), ] ```

Further instructions.

Define the queryset filters for the CRUDL endpoints

In order to control what is exposed and operable via the CRUDL endpoints, you can define the queryset filters.

This serves as an additional layer of security and control.

You can define:

  • the base queryset filter that applies to all CRUDL operations
  • the operation type specific filters that apply to the create, update, delete, list, and get_one operations separately.

The filters are defined as Django models.Q objects. If you return an empty model.Q object, no additional filtering is applied.

For security reasons, the developer needs to explicitly define the filters for each operation type. You need to explicitely override the following methods in the CRUDL controller. If you do not override them, the "not implemented error" will be raised.

```python

from django.db.models import Q from djangoninjacrudl import Crudl, RequestDetails

class MyModelCrudl(Crudl): """A CRUDL controller for the Database model."""

class Meta(Crudl.Meta):
    model_class = MyModel

    # ... #

# ... #

@override
def get_base_filter(self, request: RequestDetails) -> Q:
    """Return the base queryset filter that applies to all CRUDL operations."""
    return Q()

@override
def get_filter_for_create(self, request: RequestDetails) -> Q:
    """Return the queryset filter that applies to the create operation."""
    return Q()

@override
def get_filter_for_update(self, request: RequestDetails) -> Q:
    """Return the queryset filter that applies to the update operation."""
    return Q()

@override
def get_filter_for_delete(self, request: RequestDetails) -> Q:
    """Return the queryset filter that applies to the delete operation."""
    return Q()

@override
def get_filter_for_list(self, request: RequestDetails) -> Q:
    """Return the queryset filter that applies to the list operation."""
    return Q()

@override
def get_filter_for_get_one(self, request: RequestDetails) -> Q:
    """Return the queryset filter that applies to the get_one operation."""
    return Q()

```

The RequestDetails object contains as much information as possible about the request that is available at the time of the call.

Implement permission checks

The permission checks are for checking if the user has permission to perform the CRUDL operations for:

  • the resource (=Django model) type
  • the object (=single Django model object instance)
  • the related object (=related model object instance instance)

```python

from djangoninjacrudl import BasePermission, RequestDetails

class ResourcePermission(BasePermission): def has_permission(self, request: RequestDetails) -> bool: """Check if the user has permission for the action.

    If this method returns False, the operation is not executed.
    And the endpoint returns a 403 Forbidden response.
    """
    # implement your permission check here

def has_object_permission(self, request: RequestDetails) -> bool:
    """Check if the user has permission for the object.

    If this method returns False, the operation is not executed.
    And the endpoint returns a 404 Not Found response.
    """
    # implement your permission check here

def has_related_object_permission(self, request: RequestDetails) -> bool:
    """Check if the user has permission for the related object.

    If this method returns False, the operation is not executed.
    And the endpoint returns a 404 Not Found response.
    """
    # implement your permission check here

```

NOTE: again the RequestDetails object contains as much information as possible about the request that is available at the time of the call.

Finally, you can define the permission classes to be used in the CRUDL controller:

```python

from djangoninjacrudl import Crudl

class MyModelCrudl(Crudl):

class Meta(Crudl.Meta):
    model_class = MyModel
    permission_classes = [ResourcePermission] # <-- Add the permission classes you wish to use here

    # ... #

```

Implement the pre and post hooks (optional)

With the pre and post hooks, you can execute custom code before and after each CRUDL operation type.

This is ideal for implementing custom business logic, logging, or other custom operations that only need to apply when the objects are accessed via the REST API endpoints. If you need to implement custom logic that applies to all object access regardless where they are accessed from e.g. via Django Admin, Forms, REPL, etc., you might be better to customize the Django model or model manager methods and/or use signals.

The pre and post hooks are executed in the following order:

The pre and post hooks are defined as methods in the CRUDL controller:

```python

from typing import override

from djangoninjacrudl import Crudl, RequestDetails

class MyModelCrudl(Crudl):

class Meta(Crudl.Meta):
    model_class = MyModel

    # ... #

# ... #

@override
def pre_create(self, request: RequestDetails):
    """Do something before creating the object."""
    ...

@override
def post_create(self, request: RequestDetails):
    """Do something after creating the object."""
    ...

@override
def pre_update(self, request: RequestDetails):
    """Do something before updating the object."""
    ...

@override
def post_update(self, request: RequestDetails):
    """Do something after updating the object."""
    ...

@override
def pre_delete(self, request: RequestDetails):
    """Do something before deleting the object."""
    ...

@override
def post_delete(self, request: RequestDetails):
    """Do something after deleting the object."""
    ...

@override
def pre_list(self, request: RequestDetails):
    """Do something before listing the objects."""
    ...

@override
def post_list(self, request: RequestDetails):
    """Do something after listing the objects."""
    ...

@override
def pre_get_one(self, request: RequestDetails):
    """Do something before getting the object."""
    ...

@override
def post_get_one(self, request: RequestDetails):
    """Do something after getting the object."""
    ...

```

Again, here too, the RequestDetails object contains as much information as possible about the request that is available at the time of the call.

Implement additional REST endpoints

As Crudl class inherits Django Ninja Extra's ControllerBase & APIController decorator, you can implement additional REST endpoints in the your Crudl controller this way:

```python

from ninjaextra import apicontroller, httpget, httppost, httpput, httpdelete, httppatch, httpgeneric from djangoninjacrudl import Crudl

from .models import MyModel

@api_controller() # <-- This is required to make the additional endpoints work class MyModelCrudl(Crudl): """A CRUDL controller for the Database model."""

class Meta(Crudl.Meta):
    model_class = MyModel

    # ... #

# ... #

@http_get("/my_custom_endpoint")
def my_custom_endpoint(self, request):
    """A custom endpoint."""
    # Do something here
    return {"message": "Hello, world!"}

@http_post("/my_custom_create_endpoint")
def my_custom_create_endpoint(self, request):
    """A custom create endpoint."""
    # Do something here

@http_put("/my_custom_update_endpoint")
def my_custom_update_endpoint(self, request):
    """A custom update endpoint."""
    # Do something here

@http_patch("/my_custom_partial_update_endpoint")
def my_custom_partial_update_endpoint(self, request):
    """A custom partial update endpoint."""
    # Do something here

@http_delete("/my_custom_delete_endpoint")
def my_custom_delete_endpoint(self, request):
    """A custom delete endpoint."""
    # Do something here

```

Summary of functionality

| Operation | Base queryset filter applied | Queryset filter | haspermission(...) | hasobjectpermission(...) | hasrelatedobjectpermission(...) | Model fullclean() method called | Pre and post hook methods | | ----------------------- | ---------------------------- | --------------------------- | ------------------- | -------------------------- | ---------------------------------- | -------------------------------- | ----------------------------- | | Create | No | None | Yes | No | Yes | Yes | precreate(), postcreate() | | Retrieve | Yes | getfilterforgetone(...) | Yes | Yes | Yes | Yes | pregetone(), postgetone() | | Update / Partial update | Yes | getfilterforupdate(...) | Yes | Yes | Yes | Yes | preupdate(), postupdate() | | Delete | Yes | getfilterfordelete(...) | Yes | Yes | No | No | predelete(), postdelete() | | List | Yes | getfilterforlist(...) | Yes | No | No | No | prelist(), postlist() |

Customizing certain operations

Customizing the create operation

The create operation is done through the Django model manager create() method.

If you want to customize the create operation, you can override the create method in the model manager.

```python

from django.db import models

class MyModelManager(models.Manager):

def create(self, **kwargs):
    # Do something before creating the object
    return super().create(**kwargs)

class MyModel(models.Model): id = models.AutoField(primarykey=True) name = models.CharField(maxlength=255) description = models.TextField()

objects = MyModelManager()

```

Customizing the delete operation

The delete operation is done through the Django models' delete() method.

If you want to customize the delete operation, you can override the delete method in the model.

```python

from django.db import models

class MyModel(models.Model): id = models.AutoField(primarykey=True) name = models.CharField(maxlength=255) description = models.TextField()

def delete(self, using=None, keep_parents=False):
    # Put custom delete logic here
    # return super().delete(using=using, keep_parents=keep_parents)

```

Validations

The framework provides the following validations:

Request validation

The HTTP API request payload (JSON) structure is validated automatically using Pydantic just like it is done in Django Ninja. If the request payload does not match the expected structure, a 422 Unprocessable Entity response is returned.

Create and update validation

The models are validated before creating or updating the object using the Django model's full_clean() method.

If you want to customize the validation, you can override or customize the Django model's fullclean method in the Django model. If you need to customize the related object validation, you can override the related object's fullclean method. With many-to-many relationships, you might need to create the through model and override the full_clean method there.

Get help, support or discuss

If you need help, support or want to discuss or contribute, you can reach out via the following channels:

Ways to support this project

Owner

  • Name: NextGenContributions
  • Login: NextGenContributions
  • Kind: organization

Citation (CITATION.cff)

# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!

cff-version: 1.2.0
title: django-ninja-crudl
message: >-
  If you use this software, please cite it using the
  metadata from this file.
type: software
authors:
  - given-names: Jukka
    family-names: Hassinen
    email: jukka.hassinen@gmail.com
repository-code: 'https://github.com/NextGenContributions/django-ninja-crudl'
abstract: CRUDL REST API endpoints from Django ORM models using Django Ninja framework.
keywords:
  - Django
  - Ninja
  - CRUD
  - CRUDL
  - REST
  - API
  - OpenAPI
license: MIT

Issues and Pull Requests

Last synced: 4 months ago

All Time
  • Total issues: 25
  • Total pull requests: 105
  • Average time to close issues: 14 days
  • Average time to close pull requests: 4 days
  • Total issue authors: 2
  • Total pull request authors: 7
  • Average comments per issue: 0.0
  • Average comments per pull request: 5.83
  • Merged pull requests: 65
  • Bot issues: 0
  • Bot pull requests: 87
Past Year
  • Issues: 25
  • Pull requests: 105
  • Average time to close issues: 14 days
  • Average time to close pull requests: 4 days
  • Issue authors: 2
  • Pull request authors: 7
  • Average comments per issue: 0.0
  • Average comments per pull request: 5.83
  • Merged pull requests: 65
  • Bot issues: 0
  • Bot pull requests: 87
Top Authors
Issue Authors
  • phuongfi91 (13)
  • jhassine (12)
Pull Request Authors
  • github-actions[bot] (45)
  • dependabot[bot] (36)
  • phuongfi91 (15)
  • renovate[bot] (4)
  • jhassine (2)
  • deepsource-autofix[bot] (2)
  • Copilot (1)
Top Labels
Issue Labels
enhancement (14) bug (2)
Pull Request Labels
trunk (45) dependencies (36) python:uv (36) Review effort 3/5 (1) Review effort 2/5 (1)

Dependencies

Dockerfile docker
  • python 3.12-bookworm build
pyproject.toml pypi
.github/workflows/cicd.yml actions
  • actions/download-artifact v4 composite
  • pypa/gh-action-pypi-publish release/v1 composite
uv.lock pypi
  • 229 dependencies