Metadata-Version: 2.4
Name: zibanu-django-template
Version: 2.0.0
Summary: Zibanu repository 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: zibanu-django>=2.0.0
Dynamic: license-file

# Zibanu Django Template

Optional Zibanu Django package with helpers for the template layer: a context processor that
publishes site-wide values to every template, plus the `zb_tags` library of template tags and
filters.

## Overview

`zibanu.django.template` has **no models, no migrations and no REST endpoints**. It is a
presentation-layer package only, and it consists of three things:

- **`zb_context_data`** — the supported context processor, publishing eight site-wide keys to
  every template. `full_static_uri` and `site` are kept as **deprecated wrappers** that delegate
  to it and publish a single key each.
- **`zb_tags`** — one template tag library holding **five tags** (`zb_concat`,
  `zb_query_filter`, `zb_static_uri`, `zb_subtotal_dict`, `zb_sum_dict`) and **three filters**
  (`zb_divide`, `zb_multiply`, `zb_replace`).
- **Settings defaults** — `ZB_FULL_URI` and the five `ZB_SITE_*` keys, applied in
  `ZbDjangoTemplate.ready()`, which is the single source of those defaults.

The package imports only Django and the standard library: it depends neither on the
`zibanu.django` base package nor on any other optional Zibanu package.

## Installation

Install the package from its standalone installer:

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

Add it to `INSTALLED_APPS` in `mainApp/components/base.py` (the Django app label is
`zb_template`):

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

No `migrate` step is needed — the package defines no models.

Installing the app is **mandatory**, not optional wiring: `ZbDjangoTemplate.ready()` is what
applies the `ZB_*` setting defaults that `zb_context_data` reads, and Django only discovers the
`zb_tags` library inside an application listed in `INSTALLED_APPS`. Without it,
`{% load zb_tags %}` fails to resolve and `zb_context_data` raises `AttributeError` on the first
missing `ZB_*` setting.

## Quick Start

Wire the context processor into your `TEMPLATES` setting:

```python
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "OPTIONS": {
            "context_processors": [
                # ... Django's own processors ...
                "django.template.context_processors.request",
                "zibanu.django.template.context_processors.zb_context_data",
            ],
        },
    },
]
```

`django.template.context_processors.request` is required alongside it: the `zb_static_uri` tag
reads `request` from the render context and raises `TemplateSyntaxError` when it is absent.

Then, in a template:

```django
{% load zb_tags %}
<img src="{% zb_static_uri "img/logo.png" %}" alt="{{ site_name }}">
<p>{{ site_description }} — v{{ site_version }}</p>
```

## Features

- Eight site-wide context keys published by a single context processor.
- Absolute (scheme + host) static and media URIs, suitable for content that leaves the site —
  e-mail bodies, PDF renderings — where Django's `{% static %}` host-relative path is not enough.
- Arithmetic filters that normalize both operands through `Decimal(str(x))`, so string, `int`,
  `float` and `Decimal` template values can be mixed without float rounding artifacts.
- Fail-soft template callables: `zb_divide`, `zb_multiply` and `zb_query_filter` never raise —
  bad input renders as nothing instead of turning a page into a 500.
- Grouping and aggregation tags (`zb_subtotal_dict`, `zb_sum_dict`) for report-style templates
  with subtotal lines.
- Translatable error messages (`gettext_lazy`); catalogs live in `locale/`.

## Usage

### Context keys published by `zb_context_data`

| Key | Value |
|---|---|
| `full_static_uri` | Absolute URI of the static base, built from the request + `STATIC_URL` (falls back to `/` when `STATIC_URL` is unset or empty) |
| `full_media_uri` | Absolute URI of the media base, built from the request + `MEDIA_URL` (same `/` fallback) |
| `full_uri` | `ZB_FULL_URI` when non-empty, otherwise the request's absolute URI of `/` |
| `site_name` | `ZB_SITE_NAME` |
| `site_logo` | `ZB_SITE_LOGO` |
| `site_favicon` | `ZB_SITE_FAVICON` |
| `site_description` | `ZB_SITE_DESCRIPTION` |
| `site_version` | `ZB_SITE_VERSION` |

All eight are available in every template rendered with a `RequestContext` once the processor is
listed in `TEMPLATES["OPTIONS"]["context_processors"]`.

### Tags and filters at a glance

The kind of each name matters: a **tag** is invoked as `{% name ... %}`, a **filter** as
`{{ value|name:"arg" }}`. The table below matches the actual registrations in
`templatetags/zb_tags/__init__.py`.

| Name | Kind | Registration | Notes |
|---|---|---|---|
| `zb_concat` | tag | `register.simple_tag` | Concatenates every argument through `str()`; a `None` renders as the literal `None`. |
| `zb_query_filter` | tag | `register.simple_tag` | Filters (and optionally orders) a queryset. **Requires `as var`.** The `order` keyword is reserved for `order_by`. |
| `zb_static_uri` | tag | `register.tag` (compiled) | Takes exactly one argument. Requires `request` in the context and a non-empty `STATIC_URL`. |
| `zb_subtotal_dict` | tag | `register.simple_tag` | Groups a list of dicts by a control key and subtotals chosen keys. **Requires `as var`.** |
| `zb_sum_dict` | tag | `register.simple_tag` | Sums one key across a list of dicts; returns a formatted string, so it renders inline. |
| `zb_divide` | filter | `register.filter` | Decimal division. Returns `Decimal`, or `""` on a non-numeric operand or a zero divisor. |
| `zb_multiply` | filter | `register.filter` | Decimal multiplication. Returns `Decimal`, or `""` on a non-numeric operand. |
| `zb_replace` | filter | `register.filter` | Substring replacement driven by a single `"what\|to"` argument. |

### Basic Usage

```django
{% load zb_tags %}

{# Absolute URI of a static resource — literal or context variable #}
<img src="{% zb_static_uri "img/logo.png" %}">
<img src="{% zb_static_uri resource_path %}">

{# String concatenation #}
{% zb_concat "order-" order.id %}          {# order-42 #}

{# Arithmetic filters #}
{{ total|zb_divide:count }}
{{ price|zb_multiply:quantity }}

{# Substring replacement: the argument holds both operands, separated by a pipe #}
{{ "aaa"|zb_replace:"a|b" }}               {# bbb #}
{{ code|zb_replace:"-|" }}                 {# empty replacement deletes the substring #}

{# Aggregation that renders inline #}
Total: {% zb_sum_dict rows "amount" ",.2f" %}   {# 1,250.00 #}
```

### Tags that return data structures need `as var`

`zb_query_filter` and `zb_subtotal_dict` return a `QuerySet` and a `list` respectively. As simple
tags, their return value is rendered directly unless it is captured, so **they must be assigned
with `as <var>`** and consumed afterwards; used bare, the template prints the object's repr
(`<QuerySet [...]>`, `[{'control': ...}]`).

```django
{% zb_query_filter articles status="published" order="-created_at" as published %}
{% for article in published %}
  <li>{{ article.title }}</li>
{% endfor %}

{% zb_subtotal_dict rows "zone" "amount" as groups %}
{% for group in groups %}
  <h3>{{ group.control }}</h3>
  {% for row in group.data %}<p>{{ row.amount }}</p>{% endfor %}
  <strong>{{ group.totals.amount }}</strong>
{% endfor %}
```

`zb_concat` and `zb_sum_dict` return strings and are meant to render inline, so they do not need
`as` (they accept it, like any simple tag).

### Advanced Usage — subtotals

`zb_subtotal_dict(source_list, key_control, *args)` returns one dictionary per group, each with
`control` (the shared value of the control key), `totals` (one entry per key named in `*args`)
and `data` (the group's records, copied without the control key). Behavior worth knowing before
using it:

- **The input must already be sorted by the control key.** A group is closed only when the value
  changes between *adjacent* records, so unsorted input yields one repeated group per run of
  equal values instead of a single group.
- Records lacking the control key are grouped under `control = None`.
- Only `int`, `float` and `Decimal` values feed a subtotal; booleans are excluded on purpose, and
  any other value is ignored for the total while still being kept in `data`.
- A key listed in `*args` that never appears keeps its initial subtotal of `0`; an empty input
  yields an empty list, with no spurious empty group.
- Passing no subtotal key raises `TemplateSyntaxError` — this is the one callable of the package
  that raises by design.

`zb_sum_dict(source_list, key, format_string=None)` applies the same numeric rules over a flat
list and always returns a string: non-dict records and a non-list `source_list` are skipped
(zero sum, no error), and an invalid `format_string` falls back to `str(sum)`.

### When to use `zb_query_filter`

This tag builds a database query in the template layer. That makes the query invisible to
view-level tests, makes N+1 traversals easy to write by accident, and degrades data errors to a
silently blank region (the failure is logged as a warning through the root logger and reported as
a `None` return). Reserve it for **ad-hoc, report-style templates** where the filtering is
genuinely presentational. Anything a view, a manager or a serializer can express belongs there
instead, where the query stays testable and its failure stays visible.

Note also that `order` is consumed as a control keyword and never applied as a lookup, so a model
field actually named `order` cannot be filtered through this tag.

## API Reference

### Context processors — `zibanu.django.template.context_processors`

- **`zb_context_data(request) -> dict`** — the supported entry point. Returns the eight keys
  listed above. Raises `AttributeError` if a `ZB_*` setting is missing because the package is not
  in `INSTALLED_APPS` and `ZbDjangoTemplate.ready()` never applied the defaults.
- **`full_static_uri(request) -> dict`** — *deprecated*. Returns `{"full_static_uri": ...}` by
  delegating to `zb_context_data`. Superseded by it; kept for backwards compatibility only.
- **`site(request) -> dict`** — *deprecated*. Returns `{"site": ...}`, taking the value from
  `zb_context_data`'s `full_uri` key (note the published key differs from the source key).
  Superseded by `zb_context_data`; kept for backwards compatibility only.

New projects should wire **only** `zb_context_data`: it already publishes both values.

### Template library — `zibanu.django.template.templatetags.zb_tags`

Owns the `register` object Django loads for `{% load zb_tags %}` and exports `register`,
`StaticNodeUri` and the eight callables.

- **`zb_concat(first_arg, *args) -> str`** — concatenation of every argument coerced with
  `str()`; equals `str(first_arg)` when no extra value is given.
- **`zb_query_filter(queryset, **kwargs) -> QuerySet | str`** — applies `**kwargs` as lookups,
  except the reserved `order` key, whose value goes to `order_by`. Returns `""` when the input
  is not a queryset or a lookup/value/ordering field is invalid (`AttributeError`, `FieldError`,
  `TypeError`, `ValidationError`, `ValueError` are caught and logged). The returned queryset
  stays lazy.
- **`zb_static_uri(parse, token) -> StaticNodeUri`** — compilation function registered with
  `register.tag`; raises `TemplateSyntaxError` unless the tag receives exactly one argument.
- **`StaticNodeUri(template.Node)`** — node produced by the above. `render(context)` returns the
  absolute `STATIC_URL` followed by the resolved resource path, and raises `TemplateSyntaxError`
  at render time when the context has no `request` or when `STATIC_URL` is undefined/empty. The
  resource path is kept as a compiled filter expression, so it may be a literal, a variable or a
  filtered expression, resolved on every render.
- **`zb_subtotal_dict(source_list, key_control, *args) -> list[dict]`** — see *Advanced Usage*.
  Raises `TemplateSyntaxError` when no subtotal key is given.
- **`zb_sum_dict(source_list, key, format_string=None) -> str`** — see *Advanced Usage*.
- **`zb_divide(value, arg)`** / **`zb_multiply(value, arg)`** — decimal arithmetic over
  `Decimal(str(x))` operands; return `""` instead of raising on invalid input (and, for
  `zb_divide`, on a zero divisor).
- **`zb_replace(value, arg) -> str`** — `@stringfilter`; `arg` is `"<what>|<to>"` and must split
  into exactly two parts, otherwise `value` is returned unchanged (a silent no-op, not an error).
  An empty `<to>` deletes the substring.

`templatetags/__init__.py` is an intentional package marker with no code: it must stay in place
for `{% load zb_tags %}` to resolve, and must not define a `register` of its own.

### Application config — `zibanu.django.template.apps`

- **`ZbDjangoTemplate(AppConfig)`** — `name = "zibanu.django.template"`, `label = "zb_template"`,
  translatable `verbose_name`. `ready()` is the single source of the package setting defaults:
  it applies each `ZB_*` default with `getattr(settings, key, default)`, so any value already
  declared by the project is preserved.

## Configuration

All six settings are optional — `ZbDjangoTemplate.ready()` fills in the defaults below — and all
six are consumed by `zb_context_data`.

| Setting | Default | Purpose |
|---|---|---|
| `ZB_FULL_URI` | `""` | Canonical site URI; empty makes the processor fall back to the request's absolute URI of `/` |
| `ZB_SITE_NAME` | `"Zibanu Django"` | Site name, published as `site_name` |
| `ZB_SITE_LOGO` | `""` | Logo location, published as `site_logo` |
| `ZB_SITE_FAVICON` | `""` | Favicon location, published as `site_favicon` |
| `ZB_SITE_DESCRIPTION` | `""` | Site description, published as `site_description` |
| `ZB_SITE_VERSION` | `"0.0.0"` | Site version, published as `site_version` |

Following the project convention, declare them in their own component file under
`mainApp/components/` and include it from `mainApp/settings.py`:

```python
# mainApp/components/zb_template.py
ZB_FULL_URI = "https://app.example.com/"
ZB_SITE_NAME = "My Application"
ZB_SITE_VERSION = "2.1.0"
```

Setting `ZB_FULL_URI` explicitly is what keeps `full_uri` (and the deprecated `site` key) correct
behind a reverse proxy, or in worker-rendered output such as e-mail, where the request host is
not the public one.

Django settings read by the package but **not** owned by it: `STATIC_URL` (used by
`zb_context_data` and required, non-empty, by `zb_static_uri`) and `MEDIA_URL`.

## Testing

```bash
python manage.py test unit_tests.template
```

There is currently **no `unit_tests/template/` module** in this repository — the package sits at
**0% test coverage**. (This project has no local execution environment; tests run on the remote
server declared in `.claude/project.config.json`.)

## Requirements

- Python 3.11+
- Django

Nothing else: the package imports only `django` and the standard library (`decimal`, `logging`).
It does not depend on the `zibanu.django` base package or on any other optional Zibanu package,
so it can be installed on its own.

## License

GNU General Public License v3 — see the `LICENSE` file in this package and the headers in
`apps.py`. Developed by CQ Inversiones SAS. Copyright © 2019–2026.

# Changelog

All notable changes to this package are documented here, one section per principal
version (`x.y.z`). Pre-releases of the same version are folded into its section.

## [2.0.0] — 2026-10-01

### Added
- Bilingual (EN/ES) `README.md` documenting the package's tags, filters, settings
  and the eight context keys published by `zb_context_data`.

### Changed
- **Breaking:** the app's Django label is now the explicit `zb_template` (it was
  previously the implicit `template`, derived from the dotted app name), matching
  the naming convention of the sibling optional packages. Code that looked the app
  up by its old implicit label (e.g. `apps.get_app_config("template")`) needs
  updating.
- **Breaking:** `zb_subtotal_dict` no longer returns a placeholder group for an
  empty input list — it now correctly returns `[]` instead of a single entry with
  zeroed totals and no data.
- `full_static_uri` and `site` are now deprecated wrappers delegating to
  `zb_context_data`, the supported entry point; both keep returning the same
  keys/values as before, so existing callers are unaffected.
- `full_static_uri` and `site` now emit a `DeprecationWarning` on every call
  (`.. deprecated:: 2.0.0`); both wrappers are scheduled for removal in `3.0.0`.
- `zb_static_uri` now compiles its argument with Django's filter-expression
  compiler instead of manually stripping quotes off the raw token, which also
  allows the argument to be a template variable, not only a quoted literal.
- `zb_divide`/`zb_multiply` now normalize both operands through
  `Decimal(str(x))`, so numeric strings are accepted and mixed-type operands
  work; `zb_multiply` no longer silently repeats a string operand as a sequence
  instead of rejecting it.
- `zb_query_filter`'s `AttributeError` fallback and `zb_divide`'s
  divide-by-zero/non-numeric fallback now return `""` instead of `None`, so a
  failed calculation renders blank in a template instead of the literal string
  "None".
- `site()` now honours `ZB_FULL_URI` (by delegating to `zb_context_data`)
  instead of always returning the raw request URI, aligning it with
  `zb_context_data`'s own `full_uri`.
- **Breaking:** `zb_replace` now partitions its argument on the **first** pipe
  instead of splitting on every pipe, so the `<to>` operand may contain literal
  pipes (`"x|a|b"` replaces `x` with `a|b`). An argument with two or more pipes
  was previously a silent no-op; it now performs a replacement. A pipe-less
  argument remains a documented no-op.
- `zb_context_data` now re-raises a missing `ZB_*` setting as
  `ImproperlyConfigured` with an actionable message (add
  `zibanu.django.template` to `INSTALLED_APPS`) instead of a bare
  `AttributeError`, and memoises its dictionary on the request
  (`request._zb_context_data`) so delegating processors no longer recompute it.

### Fixed
- **Critical:** `zb_static_uri` could never render: it tested
  `hasattr(context, "request")`, which is never true on a Django template
  `Context`, so the tag always raised "requires 'request' var in context"
  regardless of whether the request context processor was enabled.
- **Critical:** `zb_context_data` carried a stray, unused import of
  `jinja2.compiler.has_safe_repr` (left over from when `ZB_FULL_URI` support was
  added in `1.0.4`). In an environment without `jinja2` installed, importing
  this module — and therefore rendering any template using the context
  processor — raised `ImportError`.
- **Critical:** `zb_sum_dict`'s numeric-type guard
  (`type(x) == int or float`) was always true regardless of `x`'s actual type
  (`or float` makes the expression unconditionally truthy), so a non-numeric
  value in the summed column raised `TypeError` instead of being skipped.
- `zb_replace` now accepts non-string arguments (coerced via `@stringfilter`/
  `str()`) instead of raising `AttributeError`.
- `zb_sum_dict`'s optional format spec is now applied inside a `try`/`except`,
  so an invalid format string degrades to the plain value instead of raising.
- Spanish translation catalog (`locale/es/`) regenerated for the renamed
  `zb_static_uri` user-facing error messages.

### Documentation
- NumPy-style docstrings added across the package.

## [1.0.4] — 2026-07-22

### Added
- `ZB_FULL_URI` setting: when set, `zb_context_data` publishes it verbatim as
  `full_uri` instead of deriving it from the current request's absolute URI —
  useful behind a reverse proxy where the request's own host doesn't match the
  public site URL.

## [1.0.3] — 2026-05-15

### Added
- `zb_context_data`, a new context processor publishing eight keys in one call:
  `full_static_uri`, `full_media_uri`, `full_uri`, `site_name`, `site_logo`,
  `site_favicon`, `site_description` and `site_version`.
- `ZB_SITE_NAME`, `ZB_SITE_LOGO`, `ZB_SITE_FAVICON`, `ZB_SITE_DESCRIPTION` and
  `ZB_SITE_VERSION` settings, defaulted in `ZbDjangoTemplate.ready()` so a
  consumer project gets them without declaring anything.

> This version went through an `alpha`/`beta` promotion cycle before its final
> release; no package changes beyond the version marker itself landed during
> that cycle.

## [1.0.2] — 2025-08-27

### Added
- `zb_query_filter` accepts an `order` keyword, applying `.order_by()` to the
  filtered queryset before returning it.

## [1.0.1] — 2025-04-01

### Added
- `zb_replace` template filter, to replace substrings in template output
  (`{{ value|zb_replace:"needle|replacement" }}`).

### Fixed
- **Critical:** `zb_divide` and `zb_multiply`, present since `1.0.0`, were
  registered with the wrong keyword argument (`function=` instead of
  `filter_func=`), which left them unusable as template filters.

## [1.0.0] — 2025-03-14

Initial release. Provides the `zb_tags` template tag/filter library
(`zb_concat`, `zb_divide`, `zb_multiply`, `zb_query_filter`, `zb_static_uri`,
`zb_subtotal_dict`, `zb_sum_dict`) and the `full_static_uri`/`site` context
processors.
