Skip to content

Django API Reference

YoroField (Model Field)

yoro.django.fields.YoroField

Bases: CharField

A Django model field that stores a Yoro geographic code.

Behaves like a CharField in the database (stores the code string), but adds:

  • Auto-encoding: assign a (lat, lon) tuple and it encodes automatically on save.
  • Validation: rejects invalid codes on form submission.
  • Widget: renders an interactive Leaflet map picker in forms.

Parameters:

Name Type Description Default
domain str

ISO country code for the Yoro domain (default "CI").

'CI'
precision int

Hilbert order — controls resolution (default 12).

12
map_height int

Height of the map widget in pixels (default 300).

300
map_attrs dict | None

Extra widget configuration (tile_url, tile_attribution).

None

Example::

class Shop(models.Model):
    name = models.CharField(max_length=100)
    address = YoroField(domain="CI", precision=12)

    # With custom map:
    address = YoroField(
        domain="CI",
        precision=14,
        map_height=400,
        map_attrs={"tile_url": "https://.../{z}/{x}/{y}.png"},
    )
Source code in src/yoro/django/fields.py
class YoroField(models.CharField):
    """A Django model field that stores a Yoro geographic code.

    Behaves like a ``CharField`` in the database (stores the code string),
    but adds:

    - **Auto-encoding**: assign a ``(lat, lon)`` tuple and it encodes
      automatically on save.
    - **Validation**: rejects invalid codes on form submission.
    - **Widget**: renders an interactive Leaflet map picker in forms.

    Args:
        domain: ISO country code for the Yoro domain (default ``"CI"``).
        precision: Hilbert order — controls resolution (default ``12``).
        map_height: Height of the map widget in pixels (default ``300``).
        map_attrs: Extra widget configuration (``tile_url``, ``tile_attribution``).

    Example::

        class Shop(models.Model):
            name = models.CharField(max_length=100)
            address = YoroField(domain="CI", precision=12)

            # With custom map:
            address = YoroField(
                domain="CI",
                precision=14,
                map_height=400,
                map_attrs={"tile_url": "https://.../{z}/{x}/{y}.png"},
            )
    """

    description = _("Yoro geographic code")

    def __init__(
        self,
        *args,
        domain: str = "CI",
        precision: int = 12,
        map_height: int = 300,
        map_attrs: dict | None = None,
        **kwargs,
    ):
        self.yoro_domain = domain_for_country(domain)
        self.yoro_precision = precision
        self.yoro_map_height = map_height
        self.yoro_map_attrs = map_attrs or {}

        kwargs.setdefault("max_length", 20)
        kwargs.setdefault("blank", True)
        kwargs.setdefault("default", "")
        kwargs.setdefault("verbose_name", _("Yoro code"))
        super().__init__(*args, **kwargs)

    def deconstruct(self) -> tuple:
        name, path, args, kwargs = super().deconstruct()
        if self.yoro_domain != "CI":
            kwargs["domain"] = self.yoro_domain
        if self.yoro_precision != 12:
            kwargs["precision"] = self.yoro_precision
        if self.yoro_map_height != 300:
            kwargs["map_height"] = self.yoro_map_height
        if self.yoro_map_attrs:
            kwargs["map_attrs"] = self.yoro_map_attrs
        # Remove defaults we set so migrations stay clean
        for key in ("max_length", "blank", "default", "verbose_name"):
            if key in kwargs:
                defaults = {"max_length": 20, "blank": True, "default": "", "verbose_name": _("Yoro code")}
                if kwargs[key] == defaults.get(key):
                    del kwargs[key]
        return name, path, args, kwargs

    def to_python(self, value) -> str:
        """Accept strings, (lat, lon) tuples, and None."""
        if value is None:
            return ""
        if isinstance(value, (tuple, list)) and len(value) == 2:
            lat, lon = float(value[0]), float(value[1])
            return yoro_encode(lat, lon, precision=self.yoro_precision, domain=self.yoro_domain)
        return str(value).strip()

    def get_prep_value(self, value):
        """Ensure tuples are encoded before hitting the database."""
        value = self.to_python(value)
        return super().get_prep_value(value)

    def pre_save(self, model_instance, add):
        """Auto-encode (lat, lon) tuples on save."""
        value = getattr(model_instance, self.attname)
        if isinstance(value, (tuple, list)) and len(value) == 2:
            value = self.to_python(value)
            setattr(model_instance, self.attname, value)
        return value

    def validate(self, value, model_instance) -> None:
        """Validate that the value is a valid Yoro code."""
        super().validate(value, model_instance)
        if not value:
            return
        try:
            yoro_decode(value)
        except ValueError as e:
            raise ValidationError(
                _("Invalid Yoro code: %(error)s"),
                code="invalid_yoro",
                params={"error": str(e)},
            )

    def formfield(self, **kwargs) -> forms.Field:
        """Use YoroWidget with the map picker by default."""
        widget = YoroWidget(
            domain=self.yoro_domain,
            precision=self.yoro_precision,
            map_height=self.yoro_map_height,
            **self.yoro_map_attrs,
        )
        defaults = {
            "form_class": YoroFormField,
            "widget": widget,
            "domain": self.yoro_domain,
        }
        defaults.update(kwargs)
        return super().formfield(**defaults)

    # --- Helper methods available on model instances ---

    class YoroDescriptor:
        """Descriptor that provides helper methods on the field value."""

        def __init__(self, field):
            self.field = field

        def __set_name__(self, owner, name):
            self.attr_name = name

        def __get__(self, obj, objtype=None):
            if obj is None:
                return self
            return obj.__dict__.get(self.attr_name, "")

        def __set__(self, obj, value):
            if isinstance(value, (tuple, list)) and len(value) == 2:
                value = yoro_encode(
                    float(value[0]), float(value[1]),
                    precision=self.field.yoro_precision,
                    domain=self.field.yoro_domain,
                )
            obj.__dict__[self.attr_name] = value

    def contribute_to_class(self, cls, name, **kwargs):
        super().contribute_to_class(cls, name, **kwargs)
        setattr(cls, name, self.YoroDescriptor(self))

__init__(*args, domain='CI', precision=12, map_height=300, map_attrs=None, **kwargs)

Source code in src/yoro/django/fields.py
def __init__(
    self,
    *args,
    domain: str = "CI",
    precision: int = 12,
    map_height: int = 300,
    map_attrs: dict | None = None,
    **kwargs,
):
    self.yoro_domain = domain_for_country(domain)
    self.yoro_precision = precision
    self.yoro_map_height = map_height
    self.yoro_map_attrs = map_attrs or {}

    kwargs.setdefault("max_length", 20)
    kwargs.setdefault("blank", True)
    kwargs.setdefault("default", "")
    kwargs.setdefault("verbose_name", _("Yoro code"))
    super().__init__(*args, **kwargs)

to_python(value)

Accept strings, (lat, lon) tuples, and None.

Source code in src/yoro/django/fields.py
def to_python(self, value) -> str:
    """Accept strings, (lat, lon) tuples, and None."""
    if value is None:
        return ""
    if isinstance(value, (tuple, list)) and len(value) == 2:
        lat, lon = float(value[0]), float(value[1])
        return yoro_encode(lat, lon, precision=self.yoro_precision, domain=self.yoro_domain)
    return str(value).strip()

validate(value, model_instance)

Validate that the value is a valid Yoro code.

Source code in src/yoro/django/fields.py
def validate(self, value, model_instance) -> None:
    """Validate that the value is a valid Yoro code."""
    super().validate(value, model_instance)
    if not value:
        return
    try:
        yoro_decode(value)
    except ValueError as e:
        raise ValidationError(
            _("Invalid Yoro code: %(error)s"),
            code="invalid_yoro",
            params={"error": str(e)},
        )

formfield(**kwargs)

Use YoroWidget with the map picker by default.

Source code in src/yoro/django/fields.py
def formfield(self, **kwargs) -> forms.Field:
    """Use YoroWidget with the map picker by default."""
    widget = YoroWidget(
        domain=self.yoro_domain,
        precision=self.yoro_precision,
        map_height=self.yoro_map_height,
        **self.yoro_map_attrs,
    )
    defaults = {
        "form_class": YoroFormField,
        "widget": widget,
        "domain": self.yoro_domain,
    }
    defaults.update(kwargs)
    return super().formfield(**defaults)

YoroFormField (Form Field)

yoro.django.fields.YoroFormField

Bases: CharField

Form field for Yoro codes with validation.

Source code in src/yoro/django/fields.py
class YoroFormField(forms.CharField):
    """Form field for Yoro codes with validation."""

    def __init__(self, *args, domain: str = "CI", **kwargs):
        self.domain = domain
        super().__init__(*args, **kwargs)

    def validate(self, value: str) -> None:
        super().validate(value)
        if not value:
            return
        try:
            yoro_decode(value)
        except ValueError as e:
            raise ValidationError(
                _("Invalid Yoro code: %(error)s"),
                code="invalid_yoro",
                params={"error": str(e)},
            )

YoroWidget (Form Widget)

yoro.django.widgets.YoroWidget

Bases: TextInput

A text input with an embedded Leaflet map for Yoro code picking.

The widget renders:

  • A monospace text input for the Yoro code
  • A Leaflet map (click to encode, auto-decode on input)
  • Resolution and coordinate info

All encoding/decoding happens client-side via the JavaScript codec. No server round-trips needed.

Parameters:

Name Type Description Default
domain str

ISO country code (default "CI").

'CI'
precision int

Hilbert order (default 12).

12
map_height int

Height of the map in pixels (default 300).

300
tile_url str

Tile provider URL template.

'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
tile_attribution str

Tile attribution text.

'© OpenStreetMap contributors'
Source code in src/yoro/django/widgets.py
class YoroWidget(forms.TextInput):
    """A text input with an embedded Leaflet map for Yoro code picking.

    The widget renders:

    - A monospace text input for the Yoro code
    - A Leaflet map (click to encode, auto-decode on input)
    - Resolution and coordinate info

    All encoding/decoding happens client-side via the JavaScript codec.
    No server round-trips needed.

    Args:
        domain: ISO country code (default ``"CI"``).
        precision: Hilbert order (default ``12``).
        map_height: Height of the map in pixels (default ``300``).
        tile_url: Tile provider URL template.
        tile_attribution: Tile attribution text.
    """

    template_name = "yoro/widgets/yoro_widget.html"

    def __init__(
        self,
        domain: str = "CI",
        precision: int = 12,
        map_height: int = 300,
        tile_url: str = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
        tile_attribution: str = "© OpenStreetMap contributors",
        attrs: dict | None = None,
    ):
        self.domain = domain
        self.precision = precision
        self.map_height = map_height
        self.tile_url = tile_url
        self.tile_attribution = tile_attribution
        super().__init__(attrs=attrs)

    class Media:
        css = {
            "all": (
                "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",
                "yoro/css/yoro-widget.css",
            ),
        }
        js = (
            "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
            "yoro/js/yoro-codec.min.js",
            "yoro/js/yoro-widget.js",
        )

    def get_context(self, name: str, value, attrs: dict) -> dict:
        context = super().get_context(name, value, attrs)
        context["widget"].update({
            "domain": self.domain,
            "precision": self.precision,
            "map_height": self.map_height,
            "tile_url": self.tile_url,
            "tile_attribution": self.tile_attribution,
        })
        return context

Model: GeoAltiusCode

yoro.django.models.GeoAltiusCode

Bases: Model

Persisted Yoro geographic address.

Source code in src/yoro/django/models.py
class GeoAltiusCode(models.Model):
    """Persisted Yoro geographic address."""

    code = models.CharField(_("Altius code"), max_length=20, unique=True, db_index=True)
    domain = models.CharField(_("domain (ISO)"), max_length=5)
    location = gis_models.PointField(_("GPS point"), srid=4326)
    cell = gis_models.PolygonField(_("Hilbert cell"), srid=4326)
    precision = models.PositiveSmallIntegerField(_("precision (order p)"))
    resolution_m = models.FloatField(_("resolution (meters)"))
    label = models.CharField(_("label"), max_length=255, blank=True, default="")
    created_at = models.DateTimeField(_("created at"), auto_now_add=True)

    class Meta:
        verbose_name = _("Yoro address")
        verbose_name_plural = _("Yoro addresses")
        ordering = ["-created_at"]

    def __str__(self) -> str:
        return self.code

Services

Yoro Django services — encode GPS, create/retrieve codes, assign to models.

Usage::

from yoro.django.services import assign_yoro, get_or_create_yoro

# Standalone
obj = get_or_create_yoro(6.8, -5.3, domain="CI")

# On a model instance (modifies in place, does NOT save)
assign_yoro(producer_instance, domain="CI")

assign_yoro(instance, domain='CI', precision=12)

Assign an Altius Code to any model instance with location and yoro fields.

Modifies the instance in place but does not save it.

Source code in src/yoro/django/services.py
def assign_yoro(instance: object, domain: str = "CI", precision: int = 12) -> GeoAltiusCode | None:
    """Assign an Altius Code to any model instance with ``location`` and ``yoro`` fields.

    Modifies the instance in place but does **not** save it.
    """
    location = getattr(instance, "location", None)
    if not location:
        return None

    lat = location.y
    lon = location.x
    obj = get_or_create_yoro(lat, lon, precision=precision, domain=domain)
    instance.yoro = obj  # type: ignore[attr-defined]
    return obj

get_or_create_yoro(lat, lon, precision=12, domain='CI')

Encode GPS coordinates and get or create a GeoAltiusCode record.

Source code in src/yoro/django/services.py
def get_or_create_yoro(
    lat: float,
    lon: float,
    precision: int = 12,
    domain: str = "CI",
) -> GeoAltiusCode:
    """Encode GPS coordinates and get or create a ``GeoAltiusCode`` record."""
    code_str = codec.encode(lat, lon, precision=precision, domain=domain)
    decoded = codec.decode(code_str)
    bounds = decoded["bounds"]
    resolution_m = codec.resolution(decoded["precision"], domain=domain)

    location = Point(lon, lat, srid=4326)
    cell = Polygon.from_bbox((
        bounds["lon_min"],
        bounds["lat_min"],
        bounds["lon_max"],
        bounds["lat_max"],
    ))
    cell.srid = 4326

    obj, _created = GeoAltiusCode.objects.get_or_create(
        code=code_str,
        defaults={
            "domain": domain,
            "location": location,
            "cell": cell,
            "precision": decoded["precision"],
            "resolution_m": resolution_m,
        },
    )
    return obj

Views

Yoro API views (plain Django — no DRF dependency).

decode_view(request)

Decode an Altius Code to coordinates and cell bounds.

GET /api/v1/yoro/decode/?code=CI-4H7A3B

Source code in src/yoro/django/views.py
@require_GET
def decode_view(request):
    """Decode an Altius Code to coordinates and cell bounds.

    ``GET /api/v1/yoro/decode/?code=CI-4H7A3B``
    """
    code_str = request.GET.get("code", "")
    if not code_str or "-" not in code_str:
        return JsonResponse(
            {"error": "code parameter required (format: XX-XXXXX)"}, status=400
        )

    try:
        decoded = codec.decode(code_str)
    except ValueError as e:
        return JsonResponse({"error": str(e)}, status=400)

    resolution_m = codec.resolution(decoded["precision"], domain=decoded["domain"])
    nbrs = codec.neighbors(code_str)

    return JsonResponse({
        "code": code_str.upper(),
        "lat": decoded["lat"],
        "lon": decoded["lon"],
        "precision": decoded["precision"],
        "domain": decoded["domain"],
        "resolution_m": resolution_m,
        "bounds": decoded["bounds"],
        "neighbors": nbrs,
    })

domains_view(request)

List all available Yoro domains.

GET /api/v1/yoro/domains/

Source code in src/yoro/django/views.py
@require_GET
def domains_view(request):
    """List all available Yoro domains.

    ``GET /api/v1/yoro/domains/``
    """
    result = {}
    for key, dom in DOMAINS.items():
        result[key] = {
            "name": dom["name"],
            "bounds": {
                "lat_min": dom["lat_min"],
                "lat_max": dom["lat_max"],
                "lon_min": dom["lon_min"],
                "lon_max": dom["lon_max"],
            },
        }
    return JsonResponse({"domains": result})

encode_view(request)

Encode lat/lon to Altius Code.

GET /api/v1/yoro/encode/?lat=6.8&lon=-5.3&domain=CI&precision=12

Source code in src/yoro/django/views.py
@require_GET
def encode_view(request):
    """Encode lat/lon to Altius Code.

    ``GET /api/v1/yoro/encode/?lat=6.8&lon=-5.3&domain=CI&precision=12``
    """
    try:
        lat = float(request.GET["lat"])
        lon = float(request.GET["lon"])
    except (KeyError, ValueError, TypeError):
        return JsonResponse({"error": "lat and lon are required"}, status=400)

    domain = codec.domain_for_country(request.GET.get("domain", "CI"))
    precision = int(request.GET.get("precision", 12))

    code_str = codec.encode(lat, lon, precision=precision, domain=domain)
    decoded = codec.decode(code_str)
    resolution_m = codec.resolution(decoded["precision"], domain=domain)

    return JsonResponse({
        "code": code_str,
        "lat": decoded["lat"],
        "lon": decoded["lon"],
        "precision": decoded["precision"],
        "resolution_m": resolution_m,
        "bounds": decoded["bounds"],
    })

precisions_view(request)

List canonical precision levels for a domain.

GET /api/v1/yoro/precisions/?domain=CI&max_length=10

Source code in src/yoro/django/views.py
@require_GET
def precisions_view(request):
    """List canonical precision levels for a domain.

    ``GET /api/v1/yoro/precisions/?domain=CI&max_length=10``
    """
    domain = codec.domain_for_country(request.GET.get("domain", "CI"))
    max_length = int(request.GET.get("max_length", 10))

    try:
        levels = codec.precision_levels(domain=domain, max_code_length=max_length)
    except ValueError as e:
        return JsonResponse({"error": str(e)}, status=400)

    return JsonResponse({"domain": domain, "levels": levels})