Metadata-Version: 2.4
Name: zibanu-django
Version: 3.0.0
Summary: Zibanu library for django projects
Author-email: Mario Cerón <mario.ceron@cqinversiones.co>
License-Expression: GPL-3.0-or-later
Keywords: django,zibanu,library,auth
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Django
Classifier: Framework :: Django :: 3.2
Classifier: Framework :: Django :: 4
Classifier: Framework :: Django :: 5
Classifier: Environment :: Web Environment
Classifier: Operating System :: OS Independent
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=3.2
Requires-Dist: djangorestframework
Requires-Dist: djangorestframework-simplejwt
Requires-Dist: Pillow
Requires-Dist: drf-extra-fields>=3.7
Dynamic: license-file

# zibanu.django

Base framework package of Zibanu Django: ORM base classes, Django REST Framework
extensions, and shared utilities that every optional package builds on.

## Overview

`zibanu.django` is the **always-installed foundation** of the Zibanu Django framework.
It has no dependency on any optional package (`auth`, `repository`, `logging`,
`template`, `gis`, `mqtt`, `state_machine`) — each of those depends on this one, never
the other way around. Its own runtime dependencies are Django, Django REST Framework,
Pillow and `drf-extra-fields`; SimpleJWT is **not** imported at module level — `ViewSet`
and `ModelViewSet` resolve `JWTTokenUserAuthentication` lazily, per request, only when
`django.apps.apps.is_installed("zibanu.django.auth")` — so the base package installs
and runs with **no optional package present at all**.

It provides four areas:

- **`zibanu.django.db`** — a `Model`/`DatedModel` base class pair adding multi-database
  routing and a recursive bulk-update helper, a `Manager` honoring that routing, a
  `PhoneField` model field, and a custom Oracle backend for reserved-word column names.
- **`zibanu.django.rest_framework`** — a `ViewSet`/`ModelViewSet` pair with this
  project's own authentication/permission defaults and error envelope, a
  `permission_required` decorator, custom exceptions, a `CurrentUserDefault` field
  compatible with SimpleJWT's `TokenUser`, and a `HybridImageField` with size
  validation.
- **`zibanu.django.lib`** — shared classes (`BaseModelController`, `CodeGenerator`,
  `ExtendedJSONEncoder`, `ModelName`, `RegEx`), the `DialCodes` enumeration, request-
  context middleware, and utility functions (mail, files, dates, model introspection,
  users, signal receivers).
- **`zibanu.django.api`** — three ready-to-mount REST endpoints: country dial codes,
  Django's configured languages, and available timezones.

## Installation

```bash
pip install ./installers/zibanu-django/
```

Add it to `INSTALLED_APPS` in your project's settings (it is required by every
optional Zibanu Django package, so in practice it is always present):

```python
INSTALLED_APPS = [
    # ...
    "zibanu.django",
]
```

`zibanu.django` does not ship any model of its own, so no migration is required just
for installing it.

## Quick Start

Build a model on the project's own base class instead of `django.db.models.Model`, to
get multi-database routing and the recursive `set()` helper for free:

```python
from zibanu.django.db.models import DatedModel, models


class Article(DatedModel):
    title = models.CharField(max_length=200)
    body = models.TextField()
```

Expose it through a `ModelViewSet`, whose actions are reached through your own
`@action`-decorated POST endpoints (see [API Reference](#api-reference)):

```python
from zibanu.django.rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action


class ArticleViewSet(ModelViewSet):
    model = Article

    @action(detail=False, methods=["post"])
    def list_articles(self, request, *args, **kwargs):
        return self._list(request, *args, **kwargs)
```

## Features

- Multi-database routing (`use_db`) built into every model without extra wiring.
- Recursive bulk-attribute assignment (`Model.set()`) that also creates, updates or
  deletes nested one-to-one/one-to-many child objects in one atomic transaction.
- A `ModelViewSet` contract that standardizes the error envelope, returns **204**
  (not 200 with an empty body) for an empty list, and centralizes exception-to-
  response translation.
- A SimpleJWT-aware `CurrentUserDefault` and `get_user()` helper, so serializer
  defaults and permission checks work whether `request.user` is a real model
  instance or a lightweight `TokenUser`.
- `FileUtils`, backed by Django's storage API, for reading/saving/inspecting files
  (MIME type, checksum, size) with optional lazy feature loading.
- `Email`, built on `EmailMultiAlternatives`, rendering HTML/text templates and
  reporting partial-delivery SMTP failures instead of treating them as success.
- Three ready-made REST endpoints: dial codes, languages, and timezones.
- A custom Oracle backend that lets a model field be named after a reserved word.
- The `after_ready`-deferred signal-wiring pattern (`ZbAppConfig`), shared by every
  optional package's own `AppConfig`, so model/DB access during signal connection
  never races Django's app-loading sequence.
- A `MinimumLevelRequired(max_level=N)` permission, granting access by privilege
  level (`get_user_level()`, lower integer = higher privilege) with a superuser
  bypass and an explicit 403 (`code="insufficient_level"`) instead of a plain
  `False` when the level can't be resolved or falls short.
- No optional-package import at module load time anywhere in the base package:
  `ViewSet`/`ModelViewSet` resolve `authentication_classes` per request, so
  `zibanu.django` starts and serves requests with zero optional packages
  installed.

## Usage

### Basic Usage

Read the configured languages and timezones once `zibanu.django.urls` is mounted
under your project (see [Configuration](#configuration)):

```
POST /utils/language/list/
POST /utils/timezone/list/
POST /utils/dial_codes/list/
```

Use the shared `ErrorMessages` and `APIException` when writing your own service
code, so errors carry this project's `{"errors": [...], "code": ...}` envelope:

```python
from zibanu.django.rest_framework.exceptions import APIException
from zibanu.django.lib.utils import ErrorMessages

raise APIException(ErrorMessages.NOT_FOUND, code="does_not_exists", status_code=404)
```

### Advanced Usage

Send a templated e-mail, resolving the current request automatically for template
context and reporting SMTP partial-delivery failures:

```python
from zibanu.django.lib.utils import Email

email = Email(subject="Welcome", to=["user@example.com"])
email.set_html_template("emails/welcome", context={"user_name": "Jane"})
email.send()
```

Inspect an uploaded file without loading it eagerly, then persist it through
Django's storage API:

```python
from zibanu.django.lib.utils import FileUtils

fu = FileUtils(
    file_dir="uploads", file_name="avatar.png",
    file_handler=uploaded_file, overwrite=True, eager_load=False,
)
fu.save()
print(fu.file_size, fu.mime_type)
```

Restrict a plain function-based or class-based view action with the project's
permission decorator (note it must go through `method_decorator` on a class-based
view — see the decorator's own docstring for why):

```python
from django.utils.decorators import method_decorator
from zibanu.django.rest_framework.decorators import permission_required


class ReportViewSet(ViewSet):
    @method_decorator(permission_required(("is_staff", "reports.view_report")))
    def list(self, request, *args, **kwargs):
        ...
```

Gate a view by privilege level instead of a fixed permission string — grants
access when the resolved level is at or above the threshold
(`level <= max_level`), or when the user is a superuser:

```python
from rest_framework.permissions import IsAuthenticated
from zibanu.django.rest_framework.permissions import MinimumLevelRequired


class InternalReportViewSet(ViewSet):
    permission_classes = [IsAuthenticated, MinimumLevelRequired(max_level=2)]
```

## API Reference

### `zibanu.django.db.models`

- **`Model`** — abstract base adding the `use_db` routing attribute and
  `set(attributes, force_update=True)`, a transactional bulk-assign that also
  walks and persists related one-to-one/one-to-many child objects.
- **`DatedModel`** — extends `Model`, adding `created_at`/`modified_at`
  (`auto_now_add`/`auto_now`).
- **`Manager`** — routes `get_queryset()` by the model's `use_db`, and adds
  `get_by_pk(pk)`, raising `Model.DoesNotExist` when no row matches.
- **`PhoneField`** — a `JSONField` storing `{"dial_code": ..., "phone_number": ...}`,
  validated against `DialCodes`.

### `zibanu.django.rest_framework.viewsets`

- **`ViewSet`** — `rest_framework.viewsets.ViewSet` defaulting to
  `IsAuthenticated`. `authentication_classes` is empty at class-body scope;
  `get_authenticators()` resolves it per request, falling back to
  `[JWTTokenUserAuthentication]` only when `zibanu.django.auth` is installed
  (and appending `TokenAuthentication` when `settings.DEBUG` is `True`) — no
  optional-package import happens at module level.
- **`ModelViewSet`** — `rest_framework.viewsets.ModelViewSet` restricted to `POST`
  (`http_method_names = ["post"]`); the public `list`/`retrieve`/`create`/
  `update`/`destroy` all raise `NotImplementedException` (405). A concrete
  subclass exposes its own `@action`-decorated POST endpoints calling the
  protected `_list`/`_retrieve`/`_create`/`_update`/`_destroy`/`_destroy_list`
  helpers, which translate ORM/validation errors into `APIException`. Same
  deferred `authentication_classes` resolution as `ViewSet`.

### `zibanu.django.rest_framework.decorators`

- **`permission_required(permissions, raise_exception=True)`** — validates
  Django permission strings (plus the special `"is_staff"` flag) against the
  resolved current user; raises `PermissionDenied` by default.

### `zibanu.django.rest_framework.exceptions`

- **`APIException`** — base exception; builds `detail = {"errors": [...], "code":
  ...}` and defaults message/code from the resolved HTTP status.
- **`NotAuthenticated`**, **`PermissionDenied`**, **`NotImplementedException`**,
  **`MultipleLoginError`** — project-specific subclasses; `ValidationError`,
  `NotFound`, `ParseError`, etc. are re-exported from DRF unchanged.

### `zibanu.django.rest_framework.fields`

- **`CurrentUserDefault`** — resolves `request.user` through `get_user()`, so it
  works whether `request.user` is a real model instance or a SimpleJWT `TokenUser`.
- **`HybridImageField`** — accepts base64 or multipart uploads, validates width/
  height against `image_width`/`image_height` context keys, and can resize the
  base64 representation via `image_size`.

### `zibanu.django.rest_framework.permissions`

- **`IsOwnerOrReadOnly`** — allows safe methods to everyone; write methods only
  when `obj.owner` (resolved via `get_user()`) matches the requesting user.
- **`MinimumLevelRequired(max_level)`** — instantiated per view (not a bare
  class in `permission_classes`); grants access to a superuser unconditionally,
  otherwise when `get_user_level()` resolves an `int` at or above the
  threshold (`level <= max_level`). Raises
  `zibanu.django.rest_framework.exceptions.PermissionDenied` with
  `code="insufficient_level"` — never a plain `False` — when the level can't
  be resolved or falls short. Implements `__call__` returning `self` so DRF's
  `get_permissions()` (which otherwise expects to instantiate each entry
  itself) accepts an already-built instance.

### `zibanu.django.lib.classes`

- **`BaseModelController`** — wraps load/save access to a model instance and its
  related child objects, driven by a `Meta` inner class validated through
  `ModelControllerMetaClass`.
- **`CodeGenerator`** — generates numeric/alphanumeric/secure codes and UUIDs for
  verification/short-lived-code use cases.
- **`ExtendedJSONEncoder`** — a `DjangoJSONEncoder` covering `FieldFile`, plain
  dates, model instances, and any duck-typed `geojson`-carrying geometry object.
- **`ModelName`** — a `str` subclass parsing an `app_label.ModelName` identifier.
- **`RegEx`** — a regular-expression pattern builder plus character-class
  constants (`LETTERS`, `DIGITS`, `PHONE_NUMBER`, `TAX_ID`, `MAC_ADDRESS`, ...).

### `zibanu.django.lib.enums`

- **`DialCodes`** — a `TextChoices` enumeration of supported country dial codes,
  with `dial_codes_list()` and `sorted(alphabetic=True)` helpers.

### `zibanu.django.lib.utils`

- **`Email`** — `EmailMultiAlternatives` subclass rendering HTML/text templates
  and reporting partial-delivery SMTP failures via `PartialSendError`.
- **`FileUtils`** — file operations backed by Django's storage API (metadata,
  checksum, save/read/delete), with optional lazy feature loading.
- **`ErrorMessages`** — reusable, translatable error-message constants.
- **`change_timezone(date, tz=None)` / `add_timezone(date, tz=None)`** — relabel
  or apply a timezone to a `datetime` value.
- **`get_user(user)` / `get_user_object(user)`** — resolve a real Django user
  (the `zibanu.django.auth` proxy when installed) from a SimpleJWT `TokenUser`.
- **`get_user_level(user) -> int | None`** — resolves `user`'s privilege level,
  always reading it fresh from the database (never a cached JWT claim): tries
  `get_user(user)`'s own `level` attribute first, then falls back to a direct
  query against `zibanu.django.auth.models.User` when that package is
  installed and the attribute was absent (covers the `TokenAuthentication`
  debug path). Never raises — any resolution failure returns `None`.
- **`get_model_list()` / `get_model_choices()` / `get_model_fields()`** — introspect
  installed Django apps for concrete models.
- **`get_ip_address(request)` / `get_http_origin(request, md5=False)`** —
  HTTP request helpers.
- **`get_receiver_id(**kwargs)` / `get_sender_name(sender)`** — signal-receiver
  helpers.
- **`object_to_list(obj)` / `import_class(dotted_name)`** — flatten a nested
  object into human-readable strings; import a symbol by dotted path.

### `zibanu.django.api.services`

- **`DialCodesViewSet`**, **`LanguageViewSet`**, **`TimeZoneViewSet`** — `POST`
  endpoints listing dial codes, Django's configured languages, and available
  timezones, mounted by `zibanu.django.urls`.

### `zibanu.django.app_config`

- **`ZbAppConfig`** — base `AppConfig` deferring signal wiring to an
  `after_ready` background thread, so model/DB access never races Django's
  app-loading sequence. Subclassed by every optional package's own
  `AppConfig`, never instantiated directly (`default = False`).

## Configuration

Wire the base package's pieces into your Django settings:

```python
MIDDLEWARE = [
    # ...
    "zibanu.django.lib.middleware.RequestContextMiddleware",
]

REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "zibanu.django.rest_framework.utils.exception_handler",
    # ...
}
```

Mount the base package's own REST endpoints:

```python
urlpatterns = [
    path("utils/", include("zibanu.django.urls")),
]
```

Relevant settings:

| Setting | Purpose |
|---|---|
| `ZB_MAIL_DEFAULT_FROM` | Default `From` address for `Email`. Falls back to `DEFAULT_FROM_EMAIL` when unset (`ZbDjango.ready()` logs when it is). |
| `ZB_MAIL_REPLY_TO` | Default `Reply-To` address for `Email`, when set. |

## Testing

```bash
python manage.py test zibanu.django.tests
```

This package's own tests live under `zibanu/django/tests/`, including the shared
test infrastructure (`test_base.py`'s `BaseTestCase`, fixtures, `fixture_path()`)
that the optional packages' own test suites import from.

## Requirements

- Python 3.9+
- Django >= 3.2
- djangorestframework
- djangorestframework-simplejwt
- Pillow
- drf-extra-fields >= 3.7

`djangorestframework-simplejwt` is pinned in the installer's own `pyproject.toml`,
but the base package never imports it at module level — `ViewSet`/`ModelViewSet`
import it lazily, per request, only once `zibanu.django.auth` is installed (see
[Features](#features)). A deployment running `zibanu.django` with no optional
package can omit it in practice, even though the installer still declares it.

Also required at runtime by specific modules, though not declared in the package's
own `pyproject.toml`: `filetype` (`FileUtils`'s MIME-type detection) and
`timezone-utils` (`TimeZoneViewSet`'s timezone choices).

## License

GPL-3.0-or-later. See the package's own `installers/zibanu-django/LICENSE`.

# Changelog

All notable changes to `zibanu.django` (the base package) are documented here, one
section per principal version (`x.y.z`). Pre-releases of the same version
(`x.y.z-dev.N`, `-alpha.N`, `-beta.N`) fold into that version's section rather than
getting a heading of their own.

Scope: this file covers `zibanu/django/api`, `db`, `lib`, `rest_framework`, `locale`,
`static`, and the package root (`__init__.py`, `apps.py`, `urls.py`) — i.e. the code
that ships as the base package. Changes confined to an optional package's own
directory (`auth/`, `repository/`, `logging/`, `template/`, `gis/`, `mqtt/`,
`state_machine/`) are out of scope and live in that package's own `CHANGELOG.md`.

## [3.0.0] — unreleased

### Added
- `zibanu.django.app_config.ZbAppConfig` — shared `AppConfig` base class centralizing
  the `ready()`/`after_ready()` deferred-signal-wiring pattern (event-wait with a
  bounded polling fallback); adopted by `zibanu.django.repository` and available for
  the other optional packages to share the same guard instead of each reimplementing it.
- `zibanu.django.lib.middleware.RequestContextMiddleware` — publishes the active
  request on a `contextvars.ContextVar` so `get_request_from_stack()` resolves it in
  O(1); the original call-stack walk remains as the fallback when the middleware isn't
  installed in a consuming project.
- `Email` (`lib/utils/mail.py`) now tolerates a partial SMTP send: a new
  `PartialSendError` exception and dedicated logging/signal path report exactly which
  recipients were refused, distinct from a full send failure.
- `get_user_level()` (`lib/utils/user_utils.py`) — a new public helper resolving a
  user's privilege level: it reads `level` duck-typed off the translated user and, when
  that is absent, falls back to a direct database lookup by primary key against the
  `zibanu.django.auth` `User` model, only if that optional package is installed. It
  never raises and never caches — any resolution failure (missing row, `zibanu.django.auth`
  not installed, a database error) is reported as `None` rather than an exception or a
  stale value. (ZBDJ-759)
- `MinimumLevelRequired` (`rest_framework/permissions/`) — a new view-level permission
  class, instantiated per view with a `max_level` threshold
  (`permission_classes = [IsAuthenticated, MinimumLevelRequired(max_level=N)]`). It
  grants access through `get_user_level()` (with an `is_superuser` bypass that never
  calls it) and denies explicitly with `PermissionDenied(code="insufficient_level")`
  rather than a plain `False`; it also implements `__call__` (returning itself) so DRF's
  `get_permissions()` accepts the pre-instantiated form shown above without raising
  `TypeError` before `has_permission` is ever reached. (ZBDJ-768, ZBDJ-772)

### Changed
- **Breaking:** `PermissionDenied` now answers HTTP 403 (Forbidden) instead of 401
  (Unauthorized), matching the docstrings and OpenAPI spec that already documented 403
  for `permission_required` rejections.
- `APIException`'s default error `code` is now derived from the response's status code
  through an internal status→code mapping when no explicit code is supplied, instead of
  a single fixed default — an uncontrolled 500 now answers a consistent code regardless
  of call site.
- `CurrentUserDefault.get_default()` now resolves the current user from `request.user`
  (it was previously handing the whole `request` object to the user-resolution helper)
  and raises instead of silently returning `None` when the serializer context carries
  no request.
- `IsOwnerOrReadOnly` now compares the object owner's primary key against the resolved
  local user instead of `request.user` directly — under this package's JWT
  authentication, `request.user` is a SimpleJWT `TokenUser`, which never compares equal
  to a real user model instance.
- `ViewSet`/`ModelViewSet` build their authenticator list per request, in
  `get_authenticators()`, instead of freezing it as a class attribute evaluated once
  when the module is first imported — `settings.DEBUG` is now honored live instead of
  at process start.
- `ModelViewSet.create`/`.update` now call the serializer's `save()` instead of calling
  `create()`/`update()` directly, so any pre/post-save hook defined on the serializer
  actually runs.
- `ModelViewSet.get_queryset` raises `ImproperlyConfigured` when a subclass doesn't
  define `model`, instead of failing later with an unrelated `AttributeError`.
- `Model.save()`/`.delete()` now honor the model's `use_db` routing attribute; `save()`
  no longer forwards a stray positional `*args` into Django's own `save()`.
- `Model.set()`: one-to-many child reconciliation now deletes rows absent from the
  payload with a single `exclude(...).delete()` instead of one `.delete()` per row;
  child-identity comparisons use `.pk` instead of `.id`, so it now works for a child
  model without Django's default `id` primary key; calling `set()` with no attributes
  at all now raises immediately instead of failing further down with an unrelated error.
- `Manager.get_by_pk()` checks existence with `.exists()` instead of materializing the
  full matching queryset.
- `PhoneField` no longer caches a row's `dial_code`/`phone_number` on the shared field
  instance (a `Field` is one object shared across every model instance, request and
  thread); `validate()` now validates a merged copy instead of mutating the caller's
  dictionary; the field gained `deconstruct()` so a non-default `dial_code` round-trips
  through migrations.
- `CodeGenerator.get_alpha_numeric_code()`/`get_secure_code()` now guarantee one
  character from each declared character class via a fixed-alphabet draw-and-shuffle,
  replacing an implementation whose effective alphabet grew from its own
  partially-generated output as it ran — so the advertised guarantee didn't actually
  hold.
- `ExtendedJSONEncoder`: a nested model value is now returned as a `dict` for the outer
  encoder to recurse into, instead of being pre-serialized to a JSON string that ended
  up double-encoded inside the payload; any `FieldFile` (not just `FileField`/
  `ImageFieldFile`) is now recognized; a `datetime` formatting bug that used `%M`
  (minutes) instead of `%m` (month) is corrected.
- `object_to_list()` no longer raises `TypeError` on a numeric error-detail value (it
  tested emptiness with `len()`), and no longer silently drops a non-string, non-proxy
  scalar value — a redundant `isinstance(value, object)` check had made an entire
  branch permanently unreachable. `get_sender_name()` (`lib/utils/receivers_utils.py`)
  carried the identical always-true `isinstance(value, object)` mistake, also fixed.
- `ModelControllerMetaClass` raises `ImproperlyConfigured` instead of relying on
  `assert` (stripped when Python runs with `-O`) for its configuration checks, requires
  a `Meta` class to be defined at all, and now copies the inherited `Meta` before
  mutating it, so a default it sets for one controller no longer leaks onto sibling
  subclasses sharing the same base `Meta`.
- `BaseModelController` now actually assigns the related-object list it builds while
  loading `kwargs` onto the instance — previously computed and discarded, making that
  whole walk dead work; it copies the incoming `kwargs` dict instead of mutating the
  caller's own, and narrows its blanket `except Exception` to `ValidationError`/
  `ValueError`, so a genuine programming error now propagates instead of being reported
  to the client as invalid input.
- `HybridImageField`: builds its guessed MIME type from `file.name` instead of
  `file.path`, which raises on any non-filesystem storage backend; width/height limits
  supplied via serializer context are now honored independently of each other instead
  of requiring both keys present at once to apply either.
- `get_user()` (`lib/utils/user_utils.py`) now imports `rest_framework_simplejwt`
  lazily inside the function body, so importing this helper no longer requires
  SimpleJWT to be installed when the caller never needed the `TokenUser` translation.
- `MultipleLoginError` now carries a default detail message ("A session is already
  open for this user.") instead of `None`.
- `DialCodesViewSet.list` no longer inverts its `sorted` request flag; `LanguageViewSet.list`
  returns a `{code, name}` list built from `settings.LANGUAGES` instead of an
  always-empty placeholder; `TimeZoneViewSet.list`'s "no results" check now tests the
  actual timezone list instead of the always-non-empty wrapper dict around it.
- `ZbDjango.ready()` no longer writes computed defaults for `ZB_MAIL_DEFAULT_FROM`/
  `ZB_MAIL_REPLY_TO` back onto the `settings` object; it now only logs when
  `ZB_MAIL_DEFAULT_FROM` is left unconfigured.
- `Email`'s `from_email`/`reply_to` settings are now read with `getattr(...)` instead
  of direct attribute access, so sending no longer raises when `ZB_MAIL_DEFAULT_FROM`/
  `ZB_MAIL_REPLY_TO` aren't configured; when no `ZB_MAIL_REPLY_TO` is set, no
  `Reply-To` header is built at all (previously it went out as the literal string
  `"None"`).
- `urls.py`: route names are now `dial_codes_list`/`timezone_list`/`language_list`
  (were free-text, human-readable strings), and the module declares
  `app_name = "zb_django"` for namespaced URL reversal.
- `ModelViewSet`/`ViewSet.authentication_classes` now default to an empty list instead
  of hardcoding `[JWTTokenUserAuthentication]`; the JWT authenticator is resolved lazily
  by `get_authenticators()`, which imports `rest_framework_simplejwt` only when
  `zibanu.django.auth` is installed. `lib/__init__.py` now imports `.enums` before
  `.classes`, breaking an import cycle (`.classes` → `zibanu.django.db.models.Model` →
  `PhoneField` → back to `.enums.DialCodes`) that only surfaced once nothing else had
  already forced this package to fully initialize first. Net effect: the base package
  now starts correctly with **no** optional package installed, where it previously
  required `zibanu.django.auth`/`rest_framework_simplejwt` to be present at import
  time regardless of whether a given deployment used JWT auth at all. (ZBDJ-752)

### Fixed
- `get_request_from_stack()` (used by audit logging on every model save/delete) now
  walks the call stack with `sys._getframe()` and stops at the first `get_response`
  frame, instead of building a full `inspect.stack()` list — which resolves source
  location for every frame — on every call; it also reads the request via
  `f_locals.get()` so a frame without that local no longer raises `KeyError`. It now
  also checks the `RequestContextMiddleware` context var first (see Added, above).
- `ExtendedJSONEncoder.default()` now serializes any value exposing a `geojson`
  attribute (duck-typed) instead of checking `isinstance(obj, GEOSGeometry)`, so the
  base package keeps no import-time dependency on `django.contrib.gis` (GDAL/GEOS) —
  installations without `zibanu.django.gis` are unaffected either way.
- `FileUtils.file_path` now goes through `FileSystemStorage.path()` (which composes via
  `safe_join`), so a traversing file name raises `SuspiciousFileOperation` instead of
  resolving to a path outside the storage root.
- `FileUtils.delete()` now returns `False`, not `True`, when there was nothing to
  delete — the previous return value contradicted its own documented contract.
- `FileUtils.save()` adopts the name the storage backend actually wrote (which can
  differ from the requested name once the backend de-duplicates it), so a later
  read/delete targets the file that was actually written.
- `FileUtils`'s extension/MIME detection no longer rewrites a file name to a bare
  trailing dot when detection produces no extension, and no longer raises on a handler
  with no `name` attribute.
- `add_timezone()` (`lib/utils/date_time.py`) now calls `django.utils.timezone.make_aware()`
  instead of a `tz.make_aware()` method that `ZoneInfo` doesn't provide.
- `ModelName.app_label`/`.model_name` now raise a clear `ValueError` for a malformed
  identifier instead of an `IndexError` from the unguarded split.
- `RegEx`'s custom-alphabet pattern builder no longer contains an always-taken dead
  branch that, in one code path, left a custom pattern unbuilt.

**Note:** in this range, `Email` (`lib/utils/mail.py`) went through an intermediate
revision that resolved the active request lazily at render time instead of capturing it
in `__init__` (closing a locmem-backend pickling failure on the stored request). A
later, larger rewrite of the same file — adding the partial-send handling described
above — reintroduced the eager `self.__request = get_request_from_stack()` capture in
`__init__`, which is the form this range ends on.

**Note:** a `2.1.3-dev.0` bump (no functional change — an `__init__.py` docstring
tweak) briefly preceded the jump straight to `3.0.0-dev.0`; `2.1.3` was never a real
development line.

## [2.1.2] — 2026-08-13

### Fixed
- Fixed a temporary-file leak in `FileUtils.__load_features`: the `file_content` code
  path created `NamedTemporaryFile(delete=False)`, leaving orphaned files under
  `/tmp`. It now uses `delete=True` (every consumer reads from the already-open
  stream, never the path), flushes the file after writing, and computes `file_size`
  from `len(file_content)` — also fixing a `0`-byte size that was previously reported
  from the unflushed buffer.

### Changed
- `__build__` is now tracked as its own field in `django/__init__.py`, independent of
  `__version__`.

## [2.1.1] — 2026-05-15

### Added
- The `zb_context_data` context processor gained `full_uri`, `full_static_uri`,
  `full_media_uri`, `site_name`, `site_logo`, `site_favicon`, `site_description` and
  `site_version`; `Email` (`lib/utils/mail.py`) now reads the current request off the
  stack to build them. (The `new_user`, `send_code` and `request_password` mail
  templates were updated accordingly — see the `auth` package's own changelog.)

### Changed
- Removed the `force_extension` flag from `FileUtils`.

## [2.1.0] — 2026-03-07

### Added
- `ModelViewSet` can now accept a pre-built `queryset` via `**kwargs`, bypassing its
  own `get_queryset()` — added to let services (e.g. a role-aware menu listing) run a
  custom filter instead of the default one.
- `lib.utils.model_utils.get_model_choices()` — a companion to `get_model_list()` that
  returns a model-name choices tuple, with an optional empty entry, for admin/form
  selects.
- `RegEx` (`lib/classes/regex.py`) extended with more building blocks for composing
  regular expressions.
- A shared Django-admin static asset: a "tabular inline scroll" widget
  (`static/zb_django/{css,js}/zb_tabular_inline_scroll.{css,js}`) for inline admin
  forms.

### Fixed
- Corrected the 404 error code for a missing object from the misspelled
  `doest_not_exists` to `does_not_exists`.
- `FileUtils` received extensive stabilization over this cycle — file-content
  handling, extension detection (`full_path_filename`, and `full_file_name` renamed
  to `full_filename`), and metadata handling — across roughly fifteen follow-up
  commits; net effect is a substantially more robust `file_utils.py`, as consumed by
  the repository package's admin view and controller.

**Note:** during this cycle `__init__.py` briefly wandered to `2.0.7-dev.*` before
returning to `2.1.0-dev.*` — an artifact of the `alpha`/`beta`/`develop` branches
being merged back and forth. No `2.0.7` was ever released; the base-package changes
committed under that stray version tag are folded into this `2.1.0` section, since
they shipped as part of the same continuous development.

## [2.0.6] — 2025-08-23

### Fixed
- Fixed an error relocating a file in `FileUtils`. (ZBDJ-275)

## [2.0.5] — 2025-08-19

### Fixed
- Fixed file deletion in `FileUtils`. (ZBDJ-266)
- Fixed a bug that allowed reassigning a PDF to an incompatible category. (ZBDJ-257)

## [2.0.4] — 2025-06-19

### Added
- `FileUtils` gained a "create version file" capability. (ZBDJ-251)

## [2.0.3] — 2025-06-16

### Fixed
- Corrected the `authentication_classes` definition in `model_viewset.py` and
  `viewset.py`.
- Fixed a bug that generated an incorrect file URL when built from the file's
  creation date.

## [2.0.2] — 2025-04-05

### Added
- `FileUtils` can now extract a file's MIME type and, for supported categories,
  embedded metadata (Office: XLSX/DOCX/PPTX; OpenDocument: ODT/ODS/ODP; images:
  PNG/JPG/TIFF; PDF) via the new `FileMetadata` class. (ZBDJ-238)

## [2.0.1] — 2025-04-01

### Fixed
- Fixed file-type validation in `FileUtils` that raised an error while editing a
  file's admin record without attaching a new file. (ZBDJ-229/230)
- Fixed the file-hash generation error in `file_utils.py`.

## [2.0.0] — 2025-03-14

### Changed
- `Model.set()` (base model mixin) now handles a nested one-to-one child that
  already has an in-memory instance but no `id`: it assigns the parent relation and
  updates it in place instead of only handling the "no instance yet" case. `save()`
  also stopped forwarding a stray `*args` to Django's own `save()`.

### Fixed
- `ExtendedJSONEncoder` fixed to correctly serialize nested models.
- `ModelViewSet._update` fixed to pass `context` to the serializer, which was
  previously omitted and broke context-dependent validation on update.

**Note:** a `1.5.2-dev.1` hotfix (cut 2025-02-06, the day `1.5.1` shipped, containing
the `FileClass`/`Model.set()` cleanup above) was superseded by this `2.0.0` line
before `1.5.2` was ever published as a stable version — `1.5.2` never shipped; its
one commit's changes are folded into this section instead.

## [1.5.1] — 2025-02-06

### Added
- New exceptions `ProtectedError` and `RestrictedError` (with Spanish translations),
  mirroring Django's own integrity-error classes; added handling for
  `UniqueConstraint` violations and for `NotAuthenticated`.

### Changed
- Password / secure-code validation (`lib/classes/code_generator.py`) reworked over
  several follow-up commits: punctuation handling corrected, minimum-length
  enforcement wired to app/settings configuration, and validation kept consistent
  between "secure" and "non-secure" password modes. (ZBDJ-154, ZBDJ-156)
- `get_request_from_stack()` now returns `None` instead of raising when no request is
  on the stack; the logging package's receivers were updated to match.
- The `MAC_ADDRESS` regex was tightened to accept only `:` as a byte separator.

### Fixed
- Corrected the `.po`/`.mo` translation build, which was silently failing to
  regenerate `django.mo`.
- Fixed a recurring bug (three follow-up attempts before it stuck) where a
  dictionary-valued error message wasn't translated when converting an object to a
  list.

## [1.5.0] — 2024-11-04

### Added
- `BaseModelController` / `ModelController` metaclass (`lib/classes/base_model_controller.py`,
  `lib/metaclasses/model_controller.py`) — a reusable base for building model
  controllers.
- `Model.set()` extended to create nested one-to-one children, to save newly created
  rows for nested one-to-many children, and to support nested ("Creación Anidada")
  and mass-delete operations. (ZBDJ-146, ZBDJ-147, ZBDJ-151, ZBDJ-152)
- `Model.set()` gained a `force_update` keyword (default `True`, preserving prior
  behavior) so a caller can let it insert a new row instead of always forcing an
  update.

### Changed
- `import_class()` (`lib/utils/generic_utils.py`) is no longer limited to locating
  serializers — it can import any class by dotted path.

### Deprecated
- `zibanu.django.utils` — its contents moved to `zibanu.django.lib.utils`. The old
  import path is kept only as a backward-compatible shim; per the commit that made
  the move, it "will disappear in a future version."

### Fixed
- `model_viewset.py` now forwards `message_dict` to `APIException` on update
  failures, instead of losing field-level validation detail.
- `Model.set()` fixed to no longer error when a related child object was created in
  an inconsistent state, and to support one-to-one parent-table lookups.
- Several fixes to the `RegEx` class powering pattern validation.

## [1.4.2] — 2024-08-21

### Added
- `ModelSerializer`/`ModelViewSet`: the create-vs-update context is now surfaced to
  the serializer (a `create` key in `context`, and an `_adding` property derived from
  it), so a serializer can tell which operation is in progress.

## [1.4.1] — 2024-08-09

### Added
- `RegEx` (`lib/classes/regex.py`) — a new class assembling common regular-expression
  patterns. (ZBDJ-141)

### Changed
- Secure-code/password generation (`lib/classes/code_generator.py`) now limits which
  symbols are used and validates generated codes/passwords against that set.
  (ZBDJ-140)

### Fixed
- Exceptions in `model_viewset.py` now surface via the `detail` parameter instead of
  `error`, aligning with the intended error-capture configuration. (ZBDJ-138)
- Two follow-up fixes to `RegEx` (an enum value and a validation bug) after its
  initial introduction.

## [1.4.0] — 2024-05-10

This is the first version tracked by the package's own `__version__` file, added
on 2024-05-03. The git tags `1.1.0`
(2023-03-17) and `1.1.1` (2023-03-31), present earlier in this repository, predate
that tracking and don't correspond to any recorded `__version__` value — see their
own sections below. Between `1.1.1` and this release (2023-04 through 2024-05, about
13 months) the base package's own surface (`api`/`db`/`lib`/`rest_framework`) has no
version marker of its own; that work is presented here by necessity, anchored on the
next real marker this history provides (the first `__version__` commit).

### Added
- `permission_required` decorator (`rest_framework/decorators.py`) — validates a
  permission without raising, returning a boolean/response instead.
- `zibanu.django.api`: public endpoints listing available timezones and languages
  (`api/services/timezone.py`, `language.py`), registered in `urls.py`.
- `HybridImageField` (`rest_framework/fields/hybrid_image.py`) — image upload
  validation with size/format checks, introduced and iterated on repeatedly.
- `PhoneField` (`db/models/fields/phone_field.py`) plus a dial-code enum and the
  `api/services/dial_codes.py` endpoint, for phone-number fields with country
  dial-code lookup/validation.
- `PermissionDenied` exception (mirroring DRF's own hierarchy) and a
  `MultipleLoginError` exception.
- `ModelViewSet.perform_authentication` override translating SimpleJWT's
  `InvalidToken` into the package's own `APIException`.

### Changed
- Consolidated the `APIViewSet`/`ModelViewSet` implementation: unified the `detail`
  payload shape and status codes across `api_exception.py`, simplified
  `APIViewSet`, and moved `exception_handler.py` into `utils.py`.
- The custom Oracle database backend (`db/backends/oracle`) was hardened for
  reserved-word field handling.

### Fixed
- Several regressions in `model_viewset.py`: `order_by` handling (twice), partial
  (`PATCH`) updates rejecting valid data, and `to_representation` returning the wrong
  parameter shape.
- Multiple translation/localization fixes across the `.po`/`.mo` catalogs.

## [1.1.1] — 2023-03-31

### Added
- `CurrentUserDefault` field (`rest_framework/fields`) — a "current user" default
  compatible with token-based authentication.

### Changed
- `ModelViewSet` now logs through the package logger.

## [1.1.0] — 2023-03-17

### Added
- Initial scaffold of the base package: `Model`/`DatedModel`/`Manager`
  (`db/models`), the custom Oracle database backend for reserved-word field names
  (`db/backends/oracle`), and the initial `rest_framework`/`apps.py` skeleton.
- `Email` utility (`utils/mail.py`) and mail-send audit-logging signals.

### Fixed
- Corrected the sender class name and completed the Spanish translation catalog.
