Metadata-Version: 2.1
Name: drf-react-by-schema
Version: 0.27.6
Summary: A Django app to easily integrate with React from models.
Home-page: https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-django
Author: Cooperativa EITA
Author-email: coletivo@eita.org.br
License: GNU General Public License version 3
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 3.2
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Requires-Python: >=3.10
Provides-Extra: redis
Provides-Extra: test
License-File: LICENSE

.. image:: https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-django/-/raw/master/banner_drf-react-by-schema_django_1280x500.png
  :width: 1280
  :alt: DRF And React By Schema - Django

This package, with the corresponding npm package with the same name, enables a django headless infrastructure for running with react very easily, directly from your models.

About
-----
React has great power in building highly responsive and dynamic interfaces for users, with great flexibility and quality.

On the other hand, Django offers a consolidated infrastructure, high performance, and a popular way to organize data models for creating, removing, and updating data (CRUD) for simple to highly complex and intricate systems.

A recognized and active library in Django, called Django Rest Framework, or simply DRF, leverages these Django characteristics to allow developers to create powerful API schemas directly related to the data model, using the concept of ``viewsets`` (analogous to ``views``) and ``serializers`` (analogous to ``forms``).

Building systems with a large number of forms for managing different processes is Django's power, but its approach to the interface, based on ``views``, ``forms``, and especially ``templates``, ``templatetags``, and ``inclusiontags``, is outdated, as every update must refresh the entire page, following a logic of HTML template rendering.

An attractive option is to use React as the client (interface) for a Django instance with DRF as the server. In other words, Django serves the data and performs operations on the data as a Headless server (i.e., without ``views``, ``forms``, or ``templates``, but only through the API).

However, a common problem with this approach is that it often requires duplicating work in the client (React) and the server (Django/DRF) to handle form interactions and validations in React that are related to the server models.

As an example, let's suppose that you want to manage products. Let's say that each product belongs to a product category. Let's assume we have the categories "fruits and vegetables" and "grains," and the products lettuce, papaya, and grapes in the "fruits and vegetables" category, and beans, rice, and fava beans in the "grains" category. In this example, we could consider that each product has attributes such as "name", "description", "unit", "price per unit," and "availability". For the categories, we could say they have attributes like "name" and "description".

In Django, the data model is created very simply: create a ``ProductCategory`` and a ``Product`` models, with a "ForeignKey" relationship indicating a one-to-many relationship between the product and the category (one category can have many products, and one product belongs to a single category).

In React, you would need to build the form, types of components (simple text input, long text input, numeric input, password input, yes/no switch input, autocomplete multiple-choice, single-choice dropdown, date pickers, etc.), validation conditions (whether it's mandatory or not, initial data, data format masks, etc.).

The problem is that this is redundant work because this information is already available in the Django model!

But how do you pass the data structure information to React? This is where *DRF and React By Schema - Django* (``drf-react-by-schema-django``) comes in. It creates a powerful schema on the server that can be accessed by React via the OPTION method.

And how do you build forms, validation conditions, and form components in React? This is where *DRF and React By Schema - React* (``drf-react-by-schema-react``) comes in, which consumes the schemas generated by ``drf-react-by-schema-django`` and automatically builds all of this for the developer with very few lines of code, while allowing complete customization of the interfaces (layout).

Furthermore, on the Django side, ``drf-react-by-schema-django`` automatically generates a full API endpoint structure directly from the app's models, along with powerful search tools, filters, nested related objects, validation, and handling of object creation, updating, and removal processes, which is a challenging task to do manually in Django DRF because it typically requires in-depth knowledge of ``serializers`` and ``viewsets``. The Django package simplifies this entire process for the developer while maintaining performance.

On the React side, ``drf-react-by-schema-react`` offers a range of powerful components that allow for the automatic construction of forms, including forms for multiple related objects (through editable tables), using the full power of MUI and MUI x-DataGrid.

The *DRF and React By Schema* Suite is available for React at https://www.npmjs.com/package/drf-react-by-schema and for Django at https://pypi.org/project/drf-react-by-schema/.

Pre-requisites
--------------
* django-rest-framework
* drf-nested-routers
* django-cors-headers
* djangorestframework-simplejwt
* django-filter

Compatibility
-------------
This package supports **Django 3.2 through 5.2** and **Python 3.10+** from a
single release line. The supported combinations are exercised in CI (see
``.gitlab-ci.yml`` / ``tox.ini``).

.. list-table::
   :header-rows: 1

   * - Django
     - Python
     - Notes
   * - 3.2
     - 3.10
     - End of upstream life. Pin ``drf-excel<2.6`` (see below).
   * - 4.2
     - 3.10 - 3.12
     - LTS
   * - 5.0
     - 3.10 - 3.12
     -
   * - 5.1
     - 3.10 - 3.13
     -
   * - 5.2
     - 3.10 - 3.13
     -

.. note::

   **Django 3.2 consumers** must additionally pin ``drf-excel<2.6``.
   ``drf-excel`` 2.6 dropped Django 3.2 support and requires Python >=3.10, and
   because ``drf-excel`` does not declare a Django constraint, pip would
   otherwise install it on Django 3.2. Django 4.2-5.2 can use the latest
   ``drf-excel`` without a cap::

       drf-react-by-schema
       drf-excel<2.6

Optional Dependencies
---------------------
For enhanced caching capabilities in the automatically generated viewsets, you can install optional dependencies:

1. **Redis Support** (recommended for production caching):

   .. code-block:: bash
   
       pip install drf-react-by-schema[redis]
   
   Then configure Redis caching in your Django settings:

   .. code-block:: python

       CACHES = {
           'default': {
               'BACKEND': 'django_redis.cache.RedisCache',
               'LOCATION': 'redis://127.0.0.1:6379/1',
               'OPTIONS': {
                   'CLIENT_CLASS': 'django_redis.client.DefaultClient',
               }
           }
       }
       
   This enables high-performance caching for the global ETag system and viewset responses.

2. **Django Native Caching System**:

   Django's native cache system (without Redis) is supported out-of-the-box. You can use any of Django's built-in cache backends:

   * **In-memory cache** (development):

     .. code-block:: python
   
         CACHES = {
             'default': {
                 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
             }
         }

   * **File-based cache**:

     .. code-block:: python
   
         CACHES = {
             'default': {
                 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
                 'LOCATION': '/var/tmp/django_cache',  # Don't forget to create the folder!
             }
         }

   * **Database cache**:

     .. code-block:: python
   
         CACHES = {
             'default': {
                 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
                 'LOCATION': 'my_cache_table',
             }
         }

   The caching features are particularly useful for performance optimization when dealing with frequently accessed data in the automatically generated viewsets and serializers.

Quick start
-----------
1. Add and configure the pre-requisite apps.

2. Add "drf-react-by-schema" to your INSTALLED_APPS setting like this:

   .. code-block:: python

       INSTALLED_APPS = [
           ...
           'drf_react_by_schema',
       ]
    
3. Configure Django Rest Framework in settings, adding the drf-react-by-schema renderer, metadata and filter backend, as in the example below:

   .. code-block:: python

       REST_FRAMEWORK = {
           'DEFAULT_PERMISSION_CLASSES': (
               'rest_framework.permissions.IsAuthenticated',
           ),
           'DEFAULT_AUTHENTICATION_CLASSES': (
               'rest_framework_simplejwt.authentication.JWTAuthentication',
               'rest_framework.authentication.SessionAuthentication',
           ),
           "DEFAULT_RENDERER_CLASSES": [
               "rest_framework.renderers.JSONRenderer",
               "drf_react_by_schema.renderers.CustomBrowsableAPIRenderer",
           ],
           'DEFAULT_METADATA_CLASS': 'drf_react_by_schema.metadata.Metadata',
           'DEFAULT_FILTER_BACKENDS': [
               'rest_framework.filters.SearchFilter',
               'drf_react_by_schema.filters.DRFReactBySchemaOrderingFilter',
           ],
       }

4. Configure drf-react-by-schema, for example adding the apps that should be considered for building the API endpoints. If not added to settings, the default will be ``['main']``. Example:

   .. code-block:: python

       DRF_REACT_BY_SCHEMA = {
           'APPS': [
               'main',
           ],
           'PAGINATION_MODE': 'server',
           'PAGINATION_PAGE_SIZE': 50,
           'PAGINATION_PAGE_SIZE_OPTIONS': [5, 10, 25, 50, 100, 200],
           'PAGINATION_MAX_PAGE_SIZE': 10000,
           'PAGINATION_MODE_NESTED_VIEWSETS': 'server',
           'PERMISSIONS': 'rest_framework.permissions.IsAuthenticated',
           'REACT_APP_DIR': 'frontend',
           # Expose GET /drbs/schema/ for MCP clients (default: on).
           'SCHEMA_ENDPOINT': True,
       }

5. Include the URL configuration in your project urls.py like this:

   .. code-block:: python

       urlpatterns = [
           ...
           path('', include('drf_react_by_schema.urls')),
       ]

6. Start the development server and visit http://127.0.0.1:8000/api/endpoints and you will see all endpoints available.

7. Create or include your React app, for example in a ``frontend`` folder. You can find a quick guide on how to install the *DRF And React By Schema - React* package in your React app at https://www.npmjs.com/package/drf-react-by-schema.

You can customize ``viewsets`` and ``serializers`` to annotate other attributes.

Global permission-aware queryset filtering
------------------------------------------
Every generated viewset already supports a ``filter_by_callback`` (and a list-only
``filter_list_by_callback``) that runs in ``get_queryset``/``list``/``excel`` and
returns ``filter(**kwargs)`` arguments (optionally with ``or_kwargs``).

Instead of subclassing each viewset to set them, you can provide a **global
fallback** callback for all generated viewsets at once:

.. code-block:: python

    DRF_REACT_BY_SCHEMA = {
        ...
        # Dotted path to ``def callback(viewset, queryset) -> dict|None``
        # (a 3-arg ``(viewset, queryset, kwargs)`` form is also supported).
        'FILTER_BY_CALLBACK': 'myapp.filters.filter_access_by_permissions',
        'FILTER_LIST_BY_CALLBACK': 'myapp.filters.filter_list_access_by_permissions',
    }

The viewset-level attribute always wins; when it is unset, the callback falls back
to the global setting (gone too, no filtering happens). A viewset can opt out of
the global fallback by setting ``use_global_filter_by_callback = False``.

Because nested and autocomplete viewsets inherit from the same base, the global
callback also applies to them — useful for scoping pickers and sub-resources to the
rows the current user may access.

Custom model fields
-------------------
This package ships drop-in replacements for the Django model fields (``CharField``,
``DecimalField``, ``ForeignKey``, ``DateTimeField``, ``FileField``, ...) that carry the
extra metadata the React widgets need. Import them from ``drf_react_by_schema`` and use
them in your models whenever a matching type exists; they behave like their Django
counterparts at the database/migration level and the added keyword arguments are read by
``metadata.py`` into the OPTIONS schema. This package offers special fields for added
control over metadata directly from model.

Relational fields (``ForeignKey``, ``ManyToManyField``, ``OneToOneField``) have two
independent, easily-confused editability flags:

* ``editable`` — Django's own attribute. ``editable=False`` means the relation is
  **never written through the API**: it is still listed and returned nested as a
  read-only ``{id, label}`` value (so forms/grids can display it), but the server ignores
  any submitted value, so raw request data cannot create, replace, or null it. Use it for
  server-managed relations such as ``created_by``. The schema reports
  ``related_editable: false`` for these fields.
* ``related_editable`` — package-specific, default ``True``. With ``True`` the user may
  create the related record **inline** from the form's autocomplete. With ``False`` the
  relation is pick-only: it can still be set to an existing record (id-based update), but
  no on-the-fly creation is offered.

They compose: an ``editable=False`` field is read-only regardless of
``related_editable``; a writable field with ``related_editable=False`` can be changed to
an existing record but cannot create a new one.

Conditional visibility
~~~~~~~~~~~~~~~~~~~~~~

``conditional_visible`` is a package-specific keyword argument available on the custom
fields. It tells the React ``<Form>``/``FormBySchema>`` and the DataGrid inline editor to
show the field only while a condition on other fields holds (and to clear its value on
save while hidden). The value is passed through to the OPTIONS schema verbatim, so it can
use any of the following shapes:

* a single ``[source_field, source_value]`` pair — the simplest case, e.g.
  ``conditional_visible=("type", "book")`` (a Python tuple serialises to a JSON array);

* a list of those pairs — combined with **OR** (show if *any* matches), e.g.
  ``conditional_visible=[("type", "book"), ("format", "paperback")]``;

* an explicit ``{"operator": "AND" | "OR", "conditions": [...]}`` expression for arbitrary,
  parenthesised logic. ``conditions`` may mix pairs and nested operator expressions, e.g.
  show ``isbn`` only when the record ``type`` is ``book`` and the ``format`` is *either*
  ``hardcover`` *or* ``paperback``::

      conditional_visible={
          "operator": "AND",
          "conditions": [
              ("type", "book"),
              {
                  "operator": "OR",
                  "conditions": [
                      ("format", "hardcover"),
                      ("format", "paperback"),
                  ],
              },
          ],
      }

A plain list of pairs is always interpreted as **OR**; use an explicit
``{"operator": "AND", ...}`` expression when every condition must hold. This keyword
combines with ``ui_required``: a conditionally visible field is required only while it is
actually shown.

Machine-readable schema (``GET /drbs/schema/``)
-----------------------------------------------
Besides the per-model ``OPTIONS`` schema and the ``sync_react`` TS/JSON snapshots
written for the React app, the package exposes the **whole picture** as a single
JSON document, meant for programmatic clients such as an MCP server:

.. code-block:: text

    GET /drbs/schema/

It is registered at the project root (a sibling of ``/autocomplete/``), so a
model named ``Schema`` cannot collide with ``/api/schema/``. It returns one
object with:

* ``stamp`` — a deterministic SHA-256 (16 hex chars) of the structural bundle,
  stable across processes and independent of row data; it changes **iff** the
  generated picture (models, fields, endpoints, system config) or the package
  version changes;
* ``drbs_version`` — the installed ``drf-react-by-schema`` version;
* ``system`` — apps, ``AUTH_USER_MODEL``, the ``DRF_REACT_BY_SCHEMA`` summary,
  static permission hints (DRF defaults, global, per-model), request
  ``conventions`` (API base, pagination, ``ordering``/``search``/``sum_rows``
  params, grid-filter operators) and ``about`` (the project's ``ABOUT.md``,
  trimmed — see below);
* ``endpoints`` — every generated route (list and nested) with its
  ``path``/``kind``/``verbose_name``/``parent_model``/``related_model``;
* ``models`` — each model's field ``schema`` (the same ``OPTIONS`` POST payload
  the React app consumes), verbose names, generated TypeScript ``interface`` and
  the model class ``docstring`` (trimmed);
* ``interfaces`` and ``enums`` — the generated TypeScript declarations.

Documentation in the bundle
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Free-form, non-UI documentation is folded into the bundle so an assistant can
*understand* the system. It is kept out of the React artifacts on purpose:
``sync_react``'s ``schemas/*.json`` stay **byte-identical** to before.

* **Model docstrings** — a model class's *own* docstring (inherited docs are
  ignored) is reported as ``models[<Model>].docstring``, trimmed at a word
  boundary to ``schema_bundle.MODEL_DOCSTRING_MAX`` characters.
* **Field descriptions** — pass ``description="..."`` to any package field
  (``CharField``, ``ForeignKey``, ``DecimalField``, ...); it is reported as
  ``models[<Model>].schema[<field>].description`` (trimmed to
  ``FIELD_DESCRIPTION_MAX``). This is **not** ``help_text``: ``help_text`` is
  rendered to end users by ``drf-react-by-schema-js``, while ``description``
  documents the field for the API client / assistant. Only values passed
  explicitly are reported — the field type's own ``description`` is never.
* **Project ``ABOUT.md``** — a project-root ``ABOUT.md`` (``settings.BASE_DIR``)
  is reported as ``system.about`` (trimmed to ``ABOUT_MAX``). Override the path
  with ``DRF_REACT_BY_SCHEMA['ABOUT_PATH']``; a missing file yields ``null``.
  Use it for *what the project is about* — not deploy/contribute instructions.

All three feed the ``stamp``, so changing them invalidates an MCP client's
cached copy; because the bundle is cached per process, a change also needs a
process restart to be picked up.

The endpoint sits behind the project's **existing auth** (the same permission
source used by the generated viewsets — ``DRF_REACT_BY_SCHEMA['PERMISSIONS']``,
defaulting to ``IsAuthenticated``; authentication comes from your DRF defaults,
so an OAuth ``read`` token works). It sets ``ETag: "<stamp>"``, honors
``If-None-Match`` (returning ``304 Not Modified``) and caches the built bundle
per process. The payload contains **structure and static configuration only** —
no row data and no secrets.

It is gated by ``DRF_REACT_BY_SCHEMA['SCHEMA_ENDPOINT']`` (default ``True``).
Set it to ``False`` to not register the route at all:

.. code-block:: python

    DRF_REACT_BY_SCHEMA = {
        ...
        'SCHEMA_ENDPOINT': False,
    }

Pagination
----------
Page size is a single source of truth in ``DRF_REACT_BY_SCHEMA``:

* ``PAGINATION_PAGE_SIZE`` — default page size, ``50`` if unset. The deprecated
  ``PAGINATION_PER_PAGE`` is still read as a fallback (with a
  ``DeprecationWarning``).
* ``PAGINATION_PAGE_SIZE_OPTIONS`` — the choices offered by the generated React
  client, ``[5, 10, 25, 50, 100, 200]`` if unset. ``sync_react`` writes the
  resolved values to ``src/drfReactBySchema/pagination.ts``
  (``export const paginationConfig``), which the app passes to
  ``DRFReactBySchemaProvider`` (``pagination`` prop).
* ``PAGINATION_MAX_PAGE_SIZE`` — hard server-side cap, ``10000`` if unset.

Clients may override the size **per request** with the ``page_size`` query
parameter (e.g. ``?page=N&page_size=200``); the value is capped at
``PAGINATION_MAX_PAGE_SIZE``. The resolved configuration is reported to MCP
clients under ``system.conventions.pagination``.

Server-side totals & facets (``sum_rows``)
------------------------------------------
List endpoints accept a ``sum_rows`` query parameter that aggregates over the
**whole filtered set** (not just the current page), regardless of which page is
returned. It is available when server pagination is on
(``DRF_REACT_BY_SCHEMA['PAGINATION_MODE'] == 'server'``); the totals ride in the
pagination envelope as ``sum_rows`` (and facet breakdowns as ``facets``).

The parameter is a comma-separated list of items. Each item is a **measure**
(optionally faceted):

.. code-block:: text

    item := measure [ '|' dim [ '|' dim ... ] [ '|' limit ] ]

* ``measure`` is a numeric field name, or the reserved ``count`` (``COUNT(*)``).
* ``dim`` is a **facet**: a ``ForeignKey`` (grouped by ``fk_id``), a field with
  ``choices``, or a ``BooleanField``. Forward relations may be nested with
  ``__`` (e.g. ``comunidade__municipio`` groups by the community's municipality).
* a trailing all-digits part is the ``limit`` (top-N buckets, ordered by the
  measure descending). Django field names can never be all-digits, so it is
  unambiguous.
* ``|`` separates the parts of an item; ``,`` separates items.

Flat totals keep their legacy shape ``{"<measure>_total": value}`` and every
measure — faceted or not — also reports its grand total there.

.. code-block:: text

    GET /api/linhadebase/?sum_rows=area                    # {area_total: ...}
    GET /api/linhadebase/?sum_rows=area,count              # two global totals
    GET /api/linhadebase/?sum_rows=count|comunidade        # nº de linhas por comunidade
    GET /api/linhadebase/?sum_rows=count|comunidade__municipio  # ... por município da comunidade
    GET /api/linhadebase/?sum_rows=area|comunidade         # área por comunidade (+ area_total)
    GET /api/linhadebase/?sum_rows=area|comunidade|cadunico  # cross-tab
    GET /api/linhadebase/?sum_rows=area|cadunico|4         # top-4 cadúnico por área

Response (the ``facets`` key is ``null`` when no facet was requested):

.. code-block:: json

    {
      "count": 120,
      "sum_rows": {"area_total": 4500.0, "count_total": 120},
      "facets": [
        {
          "field": "area",
          "by": ["comunidade"],
          "limit": null,
          "buckets": [
            {"values": {"comunidade": {"value": 12, "display_name": "Bom Jesus"}}, "total": 1200.0},
            {"values": {"comunidade": {"value": null, "display_name": "—"}}, "total": 0.0}
          ]
        }
      ],
      "results": ["..."]
    }

Each bucket carries ``values`` (one ``{value, display_name}`` per facet
dimension, using the same convention as choice fields elsewhere) and ``total``
(the measure). Invalid measures/facets (non-numeric, unknown, non-concrete) are
rejected with ``400``. The same grammar powers the ``drf-react-by-schema-mcp``
``resumo`` tool and is understood by ``drf-react-by-schema-js``.

Password reset & change
-----------------------
The package ships re-usable endpoints for the JWT + simplejwt stack. They reuse
``django.contrib.auth`` (token generator + password hashing), so no extra package
(such as allauth / dj-rest-auth) is required.

Endpoints (auto-registered, no authentication needed for ``password_reset``):

* ``POST  my_account/change_password/`` — authenticated. Body: ``old_password``, ``new_password``.
* ``POST  password_reset/request/`` — ``{email}``. Mints a reset ``uid``+``token`` for the user (if the e-mail is registered; replies generically either way to avoid leaking accounts) and forwards them to the delivery callback.
* ``POST  password_reset/confirm/`` — ``{uid, token, new_password}``. Validates the token and sets the new password.

Sending the reset e-mail is **pluggable**. The simplest path needs no code: configure the standard Django e-mail settings and the reset-link path, and a default callback sends a plain-text message via Django's mail backend:

.. code-block:: python

    # EMAIL_* (e.g. EMAIL_BACKEND, EMAIL_HOST, ...) must also be configured.
    PASSWORD_RESET_CONFIRM_PATH = '/password-reset/confirm'  # your frontend page

``request_password_reset`` looks the user up by e-mail directly (not through
``PasswordResetForm``) so accounts with an *unusable* password — e.g. freshly
invited users that still must set their own password — also receive a reset link.

For a customized delivery (branding, HTML, another channel), point
``DRF_REACT_BY_SCHEMA['PASSWORD_RESET_CALLBACK']`` at a callable or a dotted path.
The library already mints the ``uid``/``token`` before invoking your callback, so
the effective change is a single small function like the one below:

.. code-block:: python

    # myapp/auth.py
    from django.conf import settings
    from django.core.mail import send_mail

    def send_reset_email(request, user, uid, token):
        domain = request.get_host()
        scheme = 'https' if request.is_secure() else 'http'
        path = getattr(settings, 'PASSWORD_RESET_CONFIRM_PATH', '/password-reset/confirm')
        send_mail(
            'Redefinição de senha',
            f'Clique aqui para definir sua senha: {scheme}://{domain}{path}?uid={uid}&token={token}',
            None,
            [user.email],
        )

.. code-block:: python

    DRF_REACT_BY_SCHEMA = {
        ...
        'PASSWORD_RESET_CALLBACK': 'myapp.auth.send_reset_email',
        'PASSWORD_RESET_CONFIRM_PATH': '/password-reset/confirm',
    }

For any questions, please add issues in the GitLab repository.
