Metadata-Version: 2.1
Name: drf-react-by-schema
Version: 0.29.2
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 :: GNU General Public License v3 (GPLv3)
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
Description-Content-Type: text/x-rst
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

``drf-react-by-schema-django`` is the Django/DRF half of the *DRF and React By Schema*
suite. Given a Django app's models, it generates the full DRF API surface (serializers,
viewsets and routes) and a machine-readable schema that the companion React package
(``drf-react-by-schema`` on npm) consumes to build forms, tables and validation
automatically.

Overview
--------
Django and DRF are a solid foundation for defining data models and exposing them over an
API, but building the matching React UI (form widgets, validation, autocompletes,
related-object editors) duplicates information that already lives in the models. This
package removes that duplication:

* It generates one DRF ``Serializer`` and one ``ViewSet`` per model in the configured
  apps, plus the routes (``/api/<model>/``, nested related routes and autocompletes).
* It describes each model through DRF ``OPTIONS`` and exposes a machine-readable schema
  at ``GET /drbs/schema/``.
* The companion React package (``drf-react-by-schema`` on npm) consumes those schemas to
  build the forms, grids and validation, while still allowing full layout customization.
* The ``sync_react`` management command writes the schema and the TypeScript types into
  the React project.

The suite is published for React at https://www.npmjs.com/package/drf-react-by-schema and
for Django at https://pypi.org/project/drf-react-by-schema/.

Requirements
------------
Installed automatically with the package (see ``setup.cfg``):

* Django 3.2 - 5.2
* djangorestframework
* djangorestframework-simplejwt
* drf-nested-routers
* django-filter
* django-cors-headers
* drf-excel

The package itself imports with no PostgreSQL dependency. ``ArrayField`` (Django's
PostgreSQL array field) additionally needs a PostgreSQL driver (``psycopg2`` or
``psycopg``) to be importable; it is resolved lazily, so a project that does not use it
needs no driver.

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``). ``install_requires`` caps Django at
``<6.0`` so a fresh install cannot pull an untested major; the cap is raised
when the supported range is extended.

.. 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

Django 3.2 is kept as **legacy support**: the 3.2-only pin above and its CI job
will be dropped when the supported floor moves to 4.2. New code targets
4.2-5.2; Django-3.2-only branches stay isolated so the migration is a single
clean-up pass.

Installation and quick start
----------------------------
1. Install the package::

       pip install drf-react-by-schema

2. Add ``drf_react_by_schema`` (and the pre-requisite apps) to your
   ``INSTALLED_APPS`` setting:

   .. code-block:: python

       INSTALLED_APPS = [
           ...
           'drf_react_by_schema',
       ]

3. Configure Django Rest Framework with the package renderer, metadata and filter
   backend:

   .. 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``. The only required entry is the list of apps
   whose models should get an endpoint (default ``['main']``):

   .. 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],
           '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,
           # Fail (400) on a grid filter the endpoint cannot resolve, instead of
           # silently ignoring it (default: off, to tolerate a stale React layout).
           'GRID_FILTER_STRICT': False,
       }

   The full list of keys is in `Configuration reference`_ below.

5. Include the package URL configuration in your project ``urls.py``:

   .. code-block:: python

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

6. Apply the migrations. The package ships the ``Upload`` staging model used by the
   upload-first file endpoint (see `Upload-first file staging`_):

   .. code-block:: bash

       python manage.py migrate

7. Generate the React schemas and TypeScript types (see
   `Generating the React client (sync_react)`_):

   .. code-block:: bash

       python manage.py sync_react

8. Start the development server and open http://127.0.0.1:8000/api/endpoints to list
   every generated endpoint.

9. Install the React package in your React app and follow its guide:
   https://www.npmjs.com/package/drf-react-by-schema.

Configuration reference
-----------------------
``DRF_REACT_BY_SCHEMA`` is the only settings block the package reads (via
``settings.get("DRF_REACT_BY_SCHEMA", {})``):

* ``APPS`` (default ``["main"]``) — the Django app labels whose models get generated
  serializers, viewsets and routes. Consumed by ``urls.py``, ``viewsets.py``,
  ``serializers.py``, the autocomplete modules and ``sync_react``.
* ``REACT_APP_DIR`` (default ``"frontend"``) — where ``sync_react`` writes
  ``src/drfReactBySchema/``, resolved as ``os.path.join(settings.BASE_DIR, REACT_APP_DIR)``.
* ``PERMISSIONS`` — the permission source for the generated viewsets (and for
  ``/drbs/schema/`` and ``/drbs/upload/``). A dotted-path string, a list/tuple of
  paths and/or classes, or a permission class. Default
  ``rest_framework.permissions.IsAuthenticated``.
* ``PAGINATION_MODE`` / ``PAGINATION_MODE_NESTED_VIEWSETS`` — ``"server"`` (or unset)
  toggles ``DRFReactBySchemaPagination`` on the top-level and nested viewsets
  respectively. See `Pagination`_.
* ``PAGINATION_PAGE_SIZE`` (default ``50``), ``PAGINATION_PAGE_SIZE_OPTIONS``
  (default ``[5, 10, 25, 50, 100]``) and ``PAGINATION_MAX_PAGE_SIZE`` (default
  ``10000``). The deprecated ``PAGINATION_PER_PAGE`` is still read as a fallback (with a
  ``DeprecationWarning``).
* ``FILTER_BY_CALLBACK`` / ``FILTER_LIST_BY_CALLBACK`` — global fallback callbacks for
  row-scoping. See `Global permission-aware queryset filtering`_.
* ``PASSWORD_RESET_CALLBACK`` — the delivery callback for the password-reset flow. See
  `Account, password change and reset`_.
* ``SCHEMA_ENDPOINT`` (default ``True``) — whether ``GET /drbs/schema/`` is registered.
* ``ABOUT_PATH`` — path to the project ``ABOUT.md`` folded into the schema bundle
  (default ``<BASE_DIR>/ABOUT.md``). See `Machine-readable schema (GET /drbs/schema/)`_.
* ``GRID_FILTER_STRICT`` (default ``False``) — whether an unresolvable grid filter
  raises a ``400`` instead of being ignored. See `Grid filters`_.

Generating the React client (``sync_react``)
--------------------------------------------
``python manage.py sync_react`` regenerates the React client surface from the current
models, serializers and viewsets. **Run it after any model, serializer, viewset or field
change.** It deletes and recreates ``<REACT_APP_DIR>/src/drfReactBySchema/`` and writes:

* ``schemas/<Model>Schema.json`` — the ``OPTIONS`` POST schema for each model
  (``{schema, verbose_name, verbose_name_plural}``);
* ``types.ts`` — the TypeScript interfaces and enums;
* ``optionsGetters.ts`` — ``get<Enum>Label`` helpers;
* ``index.ts`` — the ``SchemaLoader`` exports consumed by the React provider;
* ``pagination.ts`` — the resolved ``paginationConfig``
  (``perPage``/``pageSizeOptions``/``maxPageSize``).

Every generated file is header-commented ``// @generated by drf-react-by-schema`` and
``/* eslint-disable */``; do not edit them by hand.

The generated JSON/TS is the contract the React package consumes. Field ``type`` values,
``model_default``, ``required``, ``read_only``, ``choices``, ``conditional_visible``,
``related_editable``, ``validators_regex``, ``date_views``, ``decimal_places``,
``model_multiline``, ``max_file_size``, geometry types and the inline-formset keys all
feed the React widgets. Model class docstrings, field ``description=`` values and
``ABOUT.md`` are deliberately kept out of these files; they ride only in the schema
bundle (see `Machine-readable schema (GET /drbs/schema/)`_).

Overriding generated serializers and viewsets
---------------------------------------------
For every model in ``DRF_REACT_BY_SCHEMA['APPS']`` the package generates a
``<Model>Serializer`` and a ``<Model>ViewSet`` (plus one nested ``<Parent>Related<Child>``
viewset per reverse foreign key and an autocomplete viewset). A plain Django model
therefore gets a full ``/api/<model>/`` CRUD with no app-side code.

To customize one, define a class with the same name in the app's ``serializers.py`` or
``viewsets.py``. The app's modules are registered before the package's, so the app's class
wins the route and the viewset's ``__init__`` swaps in the app's serializer by name:

.. code-block:: python

    # myapp/viewsets.py
    from drf_react_by_schema.viewsets import GenericViewSet

    class ProdutoViewSet(GenericViewSet):
        select_related = ["categoria"]
        filter_by_callback = filter_visible_products

The same first-seen-wins rule applies to nested and autocomplete routes, and to the
``MyAccountViewSet`` / ``MyAccountSerializer`` pair.

List vs. detail serializers
---------------------------
The generated ``XSerializer`` is the canonical serializer and serves both ``retrieve``
and ``list``/``excel``. When a detail-only field would be expensive in lists, declare it
in ``Meta.list_exclude`` on the app's general serializer:

.. code-block:: python

    from drf_react_by_schema.serializers import (
        ClienteSerializer as BaseClienteSerializer,
    )

    class ClienteSerializer(BaseClienteSerializer):
        contratacao = drf_serializers.SerializerMethodField()   # detail only
        nome = drf_serializers.CharField(read_only=True)        # both

        class Meta(BaseClienteSerializer.Meta):
            list_exclude = ("contratacao",)

The package derives (and caches) a list projection that keeps every other field,
including future additions to the general serializer. ``list_exclude`` takes serializer
field names, not model ``source`` paths; ``()`` means "the list is identical to the
detail response". An explicit list serializer remains available as an escape hatch:

.. code-block:: python

    class ClienteViewSet(GenericViewSet):
        serializer_class = ClienteSerializer
        serializer_list_class = ClienteListSerializer   # optional, full override

``XListSerializer`` classes are not generated automatically: the default is the general
serializer, so app-added fields can never silently disappear from lists.

Custom model fields
-------------------
The package ships drop-in replacements for the Django model fields 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 extra keyword arguments are read by
``metadata.py`` into the ``OPTIONS`` schema. Using a plain ``django.db.models`` field
drops that metadata and degrades the auto-built UI.

Available fields: ``CharField``, ``TextField``, ``EmailField``, ``DecimalField``,
``IntegerField``, ``BooleanField``, ``DateField``, ``DateTimeField``, ``FileField``,
``ImageField``, ``ForeignKey``, ``ManyToManyField``, ``OneToOneField``, ``ArrayField``,
the PostGIS geometry fields (``PointField``, ``PolygonField``, ``MultiPointField``,
``MultiPolygonField``, ``GeometryCollectionField``) and the serializer fields
``TypedSerializerMethodField`` and ``NestedSerializerMethodField``.

Metadata keywords (all read into the schema; all have a visible effect in the React
frontend):

* ``conditional_visible`` — show the field only while a condition on other fields holds
  (and clear its value on save while hidden). Accepts a single ``[source_field,
  source_value]`` pair, a list of pairs (OR'd), or a nested
  ``{"operator": "AND"|"OR", "conditions": [...]}`` expression for explicit logic::

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

* ``ui_required`` — makes a DB-level optional (``blank=True``) field required in the UI,
  while it is shown.
* ``related_editable`` (default ``True``) — on relational fields; with ``True`` the user
  may create the related record inline from the form's autocomplete, with ``False`` the
  relation is pick-only (still updatable to an existing record).
* ``rich_text`` (``TextField``) — the column stores HTML; the React form renders a
  WYSIWYG editor and the detail view renders sanitized HTML.
* ``pattern_format`` (``CharField`` / ``IntegerField``) — an input mask in the form and
  the grid. Named patterns: ``"cpf"``, ``"cnpj"``, ``"cep"``, and
  ``"telefone"``/``"fone"``/``"phone"``/``"contact"``/``"contato"``; any other string is
  used as a literal mask where each ``#`` is one digit. On a ``CharField`` the mask also
  sets ``max_length`` to its digit count and adds a matching server-side
  ``RegexValidator``; an ``IntegerField`` uses the mask for the widget only.
* ``is_currency`` / ``prefix`` / ``suffix`` (``DecimalField``) — render a currency/number
  input. ``is_currency`` is auto-set when both ``prefix`` and ``suffix`` are empty.
* ``views`` (``DateField``) — the MUI date-picker views (e.g. ``['year', 'month',
  'day']``), emitted in the schema as ``date_views``.
* ``allowed_mime_types`` and ``max_file_size`` (``FileField`` / ``ImageField``) — filter
  the file picker and enforce size/type on the server.
* ``description`` — documentation only, never shown in the UI; it is read into the schema
  bundle's per-field ``description``. Distinct from ``help_text`` (which the React UI
  renders).

Relational editability uses two independent 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, but the server ignores any submitted value. 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 ``False`` the relation
  can 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.

The package also ships an abstract ``GenericSimpleModel`` (a ``nome`` plus an
auto-slugged unique ``slug``) for label-style models.

PostGIS / geometry fields
~~~~~~~~~~~~~~~~~~~~~~~~~
The custom fields include the GeoDjango geometry types (``PointField``,
``PolygonField``, ``MultiPointField``, ``MultiPolygonField``, ``GeometryCollectionField``).
A model geometry field is serialized as a GeoJSON ``GISGeometryField`` (the React point
picker / multipolygon draw widget) instead of DRF's generic fallback.

GeoDjango is imported **only when a model field actually is a GeoDjango field** (detected
from the field's class), never for plain fields. The geometry fields are also exported
lazily, so importing ``drf_react_by_schema`` / ``drf_react_by_schema.fields`` never touches
GeoDjango unless one is used. This matters because importing GeoDjango triggers GDAL
discovery, which on a host without GDAL invokes the C compiler
(``ctypes.util.find_library`` runs ``gcc``); doing that for plain fields makes every list
serialization extremely slow.

If a project *does* use geometry fields but GEOS/GDAL are unavailable, the failure is loud
rather than silent:

* a Django system check (``drf_react_by_schema.E001``) fails ``manage.py check`` /
  ``runserver`` / ``migrate`` with an actionable message;
* accessing ``drf_react_by_schema.fields.PointField`` (or any geometry field) raises
  ``ImproperlyConfigured`` explaining that GeoDjango failed to import.

To use geometry fields: install GeoDjango's native libraries (GDAL/GEOS, e.g.
``libgdal-dev`` and ``libgeos-dev`` on Debian/Ubuntu), add ``django.contrib.gis`` to
``INSTALLED_APPS``, and use the ``django.contrib.gis.db.backends.postgis`` database
engine. You may also set ``GDAL_LIBRARY_PATH`` / ``GEOS_LIBRARY_PATH`` to skip library
discovery.

Writing related objects (inline nested formsets)
------------------------------------------------
Declare a writable nested serializer on the parent to persist child rows in the same
request (an inline formset):

.. code-block:: python

    class TripSerializer(GenericSerializer):
        passengers = PassengerSerializer(many=True, required=False, layout='row')

        class Meta:
            model = Trip
            fields = "__all__"

The child model must have a read-only ``ForeignKey`` / ``OneToOneField`` back to the
parent. The server sets that back-FK and the client hides it (the schema flags it
``auto_managed: true``). On write the package applies the implicit diff of the rows the
client sent:

* rows without an ``id`` are created;
* rows with an ``id`` are updated, scoped to the parent;
* children missing from the payload are deleted;
* an empty list ``[]`` deletes every child, an absent key changes nothing, and an empty
  object ``{}`` on a single (one-to-one) child means "no object".

The whole diff runs in a transaction. The inline widget is configured with ``layout``
(``'row'`` or ``'accordion'``; default ``row`` for up to four visible child fields, else
``accordion``), ``min_rows`` and ``max_rows``. A writable nested serializer on a concrete
FK/O2O/M2M field name is the older "id carrier" pattern (the value is validated into an
instance and saved as is), not a formset.

For a computed value that is itself an object or list, use
``NestedSerializerMethodField(SomeSerializer, many=True)``: the method's return value is
serialized through the nested serializer, so the schema and TypeScript describe the
object instead of a bare string.

Global permission-aware queryset filtering
------------------------------------------
Every generated viewset 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, 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, which is useful for scoping pickers and sub-resources to the rows
the current user may access.

Accent-insensitive search
-------------------------
On PostgreSQL with the ``unaccent`` extension enabled (and
``django.contrib.postgres`` in ``INSTALLED_APPS``), replace DRF's search filter with the
package's:

.. code-block:: python

    REST_FRAMEWORK = {
        "DEFAULT_FILTER_BACKENDS": [
            "drf_react_by_schema.filters.DRFReactBySchemaSearchFilter",
            "drf_react_by_schema.filters.DRFReactBySchemaOrderingFilter",
        ],
    }

``unaccent`` is applied only to text fields; dates, numbers and booleans keep DRF's
default lookup, because ``unaccent`` raises on them.

The filter also orders the hits by relevance. It concatenates the short text
(``CharField``) paths of ``search_fields``, in order, and scores the whole query against
that label: exact match (100), prefix (80), substring (60). The first path is scored on
its own as well, so ``nome`` keeps its exact/prefix bonus even when the label also
contains phone/e-mail fields. Records that only match all the terms across fields keep
score 0 and are ordered by ``pk`` as a stable tiebreaker. An explicit ``?ordering=``
always wins over relevance. ``TextField`` paths (long free text such as notes) stay out of
the label.

The filter is **opt-in**: apps that keep DRF's ``SearchFilter`` are unaffected and do not
need the extension. It probes the backend, ``INSTALLED_APPS`` and the database extension
once per connection alias. If the extension is missing — or the database is not
PostgreSQL, or ``django.contrib.postgres`` is not installed — it falls back to DRF's
normal lookups: search keeps working (accent-sensitive) instead of raising ``FieldError``.
Call ``drf_react_by_schema.filters.reset_unaccent_cache()`` if the extension is installed
while the process is running.

To create the extension, add a migration in the consuming app::

    from django.contrib.postgres.operations import UnaccentExtension

    class Migration(migrations.Migration):
        operations = [UnaccentExtension()]

Grid filters
------------
The grid filter (``field`` / ``operator`` / ``value``, each a list, combined with
``AND``) accepts the operators listed in ``system.conventions.grid_filter`` of the schema
bundle: ``equals``, ``not``, ``contains``, ``startsWith``, ``endsWith``, ``isEmpty``,
``isNotEmpty``, ``isAnyOf``, ``is``, ``after``, ``before``, ``onOrAfter``, ``onOrBefore``,
``entre``, ``=``, ``!=``, ``>``, ``>=``, ``<``, ``<=``. A note on relations: equality on
a foreign key compares the related object's **label** (its ``__str__``/``nome``/``slug``),
not its primary key — to filter by pk use ``field='<fk>__id'`` (or ``field='<fk>_id'`` on
a raw model field) with operator ``is`` or ``=``.

Date operators (``is``, ``not``, ``after``, ``before``, ``onOrAfter``, ``onOrBefore``,
``entre``) are day-based when the target is a ``DateTimeField`` and the value is a bare
date (``YYYY-MM-DD``): the package applies the ``__date`` transform itself, so
``entrada_prevista`` with ``onOrBefore=2026-10-14`` includes rows later that day.
``after``/``before`` are strict (they exclude the named day); ``onOrAfter``/``onOrBefore``
are inclusive. Send a time-bearing value to keep time-of-day semantics; an explicit
``__date`` path is left untouched and remains day-based.

With ``USE_TZ=True`` the day is evaluated in the current timezone (normally
``settings.TIME_ZONE``); with ``USE_TZ=False`` the stored naive datetime is used.
``__date`` casts the column, so it cannot use a plain btree index: endpoints over large
tables should use a ``filter_by_callback`` with half-open ranges or an expression index.
``equals`` and ``isAnyOf`` are text/choice operators, not date operators; use ``is`` for
date/time columns.

With ``DRF_REACT_BY_SCHEMA['GRID_FILTER_STRICT'] = True`` a filter the endpoint cannot
resolve raises a ``400`` naming the field instead of being silently ignored (which would
return the unfiltered list). It is off by default so a stale React grid layout does not
break the list.

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]`` if unset. ``sync_react`` writes the resolved values to
  ``src/drfReactBySchema/pagination.ts`` (``export const paginationConfig``), which the
  app passes to ``DRFReactBySchemaProvider`` (``pagination`` prop). The ``maxPageSize``
  baked into that file is capped at ``500`` (the MIT DataGrid rejects a larger
  ``pageSize``); the server accepts more.
* ``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, ``count`` (``COUNT(*)``, i.e. **rows**),
  or ``count_distinct:<field>`` (``COUNT(DISTINCT <field>)``, counting distinct
  non-null values).
* ``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. ``categoria__regiao`` groups by the category's region).
* 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. The ``:`` of a
``count_distinct:<field>`` measure becomes ``_`` in the flat key (e.g.
``count_distinct_categoria_total``).

.. code-block:: text

    GET /api/widget/?sum_rows=preco                        # {preco_total: ...}
    GET /api/widget/?sum_rows=preco,count                  # two global totals
    GET /api/widget/?sum_rows=count|categoria              # rows per category
    GET /api/widget/?sum_rows=count|categoria__regiao      # ... by the category's region
    GET /api/widget/?sum_rows=count_distinct:categoria     # distinct categories
    GET /api/widget/?sum_rows=count_distinct:categoria|tipo  # distinct categories per type
    GET /api/widget/?sum_rows=preco|categoria              # price per category (+ preco_total)
    GET /api/widget/?sum_rows=preco|categoria|tipo         # cross-tab
    GET /api/widget/?sum_rows=preco|tipo|4                 # top-4 types by price

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

.. code-block:: json

    {
      "count": 120,
      "sum_rows": {"preco_total": 4500.0, "count_total": 120},
      "facets": [
        {
          "field": "preco",
          "by": ["categoria"],
          "limit": null,
          "buckets": [
            {"values": {"categoria": {"value": 12, "display_name": "Exemplo"}}, "total": 1200.0},
            {"values": {"categoria": {"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``, and the error **enumerates the valid options** for that
model (its numeric fields, its facetable foreign keys / choice / boolean
fields, or its distinct-count fields). The valid options are also published per
model in the schema bundle as
``models[<Model>].sum_rows = {"medidas": [...], "dimensoes": [...], "medidas_distintas": [...]}``.
The same grammar powers the ``drf-react-by-schema-mcp`` ``resumo`` tool and is understood
by the React package.

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 a **schema-only** 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 the relation-filter note) and ``about`` (the
  project's ``ABOUT.md``, trimmed — see below);
* ``endpoints`` — every **top-level** route with its ``path``/``kind``,
  ``verbose_name``/``verbose_name_plural``, the underlying Django ``model`` and
  ``app_label`` (which can differ from the endpoint key when a viewset aliases
  another model's) and the endpoint's **query capabilities**: ``search_fields`` /
  ``ordering_fields`` (the model paths ``?search=``/``?ordering=`` accept),
  ``filter_fields`` (the names the grid filter accepts as ``field``) and
  ``label_field`` (the field an equality filter compares when it targets a
  relation by label);
* ``models`` — each model's field ``schema`` (the ``OPTIONS`` POST payload the
  React app consumes, minus the React-only keys — see below), the Django
  ``model``/``app_label``, verbose names, the model class ``docstring`` (trimmed),
  the valid ``sum_rows`` measures and dimensions (``medidas``/``dimensoes``) and
  the model's **reverse relations** (``related``): for each child relation, its
  ``name``, the child ``model``, the linking ``field``, the child ``endpoint`` and
  — when the relation has a generated nested route — the ``nested_path`` template
  (e.g. ``/api/categoria/<pk>/widget/``).

The bundle deliberately **omits** the React-only TypeScript artifacts
(``interfaces``/``enums`` — still written to ``types.ts`` by ``sync_react``), the
React-only field keys (``schema_bundle.UI_ONLY_FIELD_KEYS``: ``ui_required``,
``model_required``, ``style``, ``model_multiline``, ``model_html``, ``is_currency``,
``prefix``/``suffix``, ``date_views``... — they carry no API semantics) and the
auto-generated nested ``*Related*`` viewsets. Instead, each relation field in
``schema`` carries a ``related_model`` naming the model it points at, the
endpoint carries its query capabilities and the model carries its reverse
relations. The React artifacts are untouched: the slimming and the extra keys
live only in the MCP bundle, so ``sync_react``'s output stays byte-identical.

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** (the bundle is a separate,
structural copy — see ``build_schema_bundle``).

* **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 the React package, 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,
    }

Upload-first file staging
-------------------------
A file inside an inline formset row is uploaded **before** the parent form is saved, so
the row can carry a plain reference and the request stays JSON (a top-level file still
forces multipart; mixing is fine, because the row reference is plain JSON inside
``key[]``).

* ``POST /drbs/upload/`` — multipart ``file`` in, ``201 {id, url, name, size}`` out. The
  permissions are the same as the generated viewsets
  (``DRF_REACT_BY_SCHEMA['PERMISSIONS']``). It is registered at the project root, so a
  model named ``Upload`` (route ``/api/upload/``) cannot collide with it. The endpoint
  requires the package migrations (see step 6 of the quick start).
* Model ``FileField`` / ``ImageField`` serializer fields accept either an
  ``UploadedFile`` (multipart, unchanged) or a reference — the whole ``{id, ...}`` object
  or a bare id. The model write **copies** the staged file into the target field (so that
  field's ``upload_to``/storage apply) and enforces its ``max_file_size`` /
  ``allowed_mime_types`` (a violation surfaces as a row-indexed ``400``).
* Once the write succeeds, the staging records and files are deleted. A failed request
  keeps the staged file so the client can retry with the same id.

Account, password change and reset
----------------------------------
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; ``password_reset`` needs no authentication):

* ``GET  /api/my_account/`` — authenticated. Returns ``MyAccountSerializer`` for the
  current user (``id``, ``username``, ``first_name``, ``last_name``, ``email``,
  ``is_active``). Override it by defining your own ``MyAccountSerializer`` /
  ``MyAccountViewSet`` in the app.
* ``POST /api/my_account/change_password/`` — authenticated. Body: ``old_password``,
  ``new_password``.
* ``POST /api/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 /api/password_reset/validate/`` — ``{uid, token}``. Resolves the account behind
  a link (name + whether a password is already set) without changing anything, so the
  frontend can greet the user and tell a first-time invite from a reset.
* ``POST /api/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
    # Optional: override the link's domain/scheme instead of using the request's.
    # PASSWORD_RESET_DOMAIN = 'example.com'
    # PASSWORD_RESET_SCHEME = 'https'

``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(
            'Password reset',
            f'Follow this link to set your password: {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',
    }

Excel export
------------
Every generated viewset exposes ``GET /api/<model>/excel/``, which returns an XLSX file
via ``drf-excel`` applying the same list filters. The download filename is derived from
the model's ``verbose_name_plural``.

Caching and ETags
-----------------
``list`` and ``retrieve`` responses are cached for 900 seconds and guarded by a single
global ETag, so a client can revalidate with ``If-None-Match``. The cache key is scoped
per authenticated user (and ``Vary: Authorization`` is set), so a row-scoped response is
never served to a different user. A successful mutation clears the cache and increments
the global ETag version. Redis is recommended for production (see
`Optional dependencies`_); Django's other cache backends work out of the box.

Optional dependencies
---------------------
For enhanced caching in the automatically generated viewsets:

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

       pip install drf-react-by-schema[redis]

   Then configure it 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',
               }
           }
       }

2. **Django's native cache backends** (in-memory, file-based, database, ...) are
   supported out of the box. For example:

   .. code-block:: python

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

Running the tests
-----------------
The suite is self-contained: ``tests/settings.py`` configures a minimal Django
project and ``tests/testapp`` provides the models the tests exercise, so nothing
outside this repository is needed. Install the package with its ``test`` extra and
run ``pytest``::

    python -m venv .venv
    . .venv/bin/activate
    pip install -e ".[test]"
    pytest -q

``install_requires`` pins ``Django>=3.2,<6.0``, so this resolves to the newest
supported series (5.2). Pin an explicit series if you need a different one, e.g.
``pip install -e ".[test]" "Django>=4.2,<4.3"``.

To reproduce the full supported matrix locally, use ``tox`` (it creates and
manages its own per-interpreter environments and pins each Django series, so no
venv is required)::

    tox -e py312-django52

``tox -l`` lists every combination; CI runs the same matrix on every push.

.. note::

   A consuming project can point at this checkout to use unpublished changes —
   an editable install (``pip install -e /path/to/drf-react-by-schema-django``)
   or a symlink of the package into the app's source tree. That is a *consumer*
   workflow and is independent of the suite above: the package's own tests always
   run from this repository.

Issues and source
-----------------
Report bugs and request features at
https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-django/-/issues.
The source is at
https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-django.
