ferrosoft.apps.ferrobase package

Ferrobase — the load-bearing foundation of the Ferrosoft platform.

Ferrobase is the framework on which every other Ferrosoft application is built. It owns the multi-tenant request pipeline, the identity and access model, the subscription and feature-gating system, the file substrate, the shared reference data, the dependency-injection container, the real-time frontend update channel, and the menu / settings extension points. Other apps register against ferrobase; ferrobase imports none of them. If a Ferrosoft application is a storey of the building, ferrobase is the slab and the steel.

Domain overview

The ferrobase domain centres on a small set of long-lived entities that every other app links against:

  • Organisations and tenants. An Organization is a customer; it owns one or more Tenant workspaces. Each tenant is fully isolated — its own PostgreSQL role, its own database, its own configuration — and is selected per request by the multi-tenant middleware (see below).

  • Identity and access. User extends Django’s AbstractUser and is backed by ZITADEL for authentication. APIToken covers machine access. One-time flows like password reset and account activation are issued as access-key documents (PasswordResetRequest, AccountActivationRequest) built on AccessKeyMixin.

  • Subscriptions and feature gating. A SubscriptionPlan pairs an organisation with the set of AppFeatures it has licensed; FeatureAssignment binds those features to individual tenants. Platform apps consult the assignment before exposing menu entries or routes, so the same codebase can serve customers on different tiers without branching.

  • Files. A StoredFile lives inside a Directory with optional FileAccessGrant rows scoping visibility per user or organisation; content is delegated to a pluggable storage backend so file data never sits in the application database.

  • Shared reference data. Country, Language, TimeZone, Material, BusinessPartner, CompanySite, Salutation, and the IDSequence / IdSequenceConfig pair back the drop-downs, look-ups, and human-readable identifiers that every other app reuses.

Multi-tenancy

Tenancy is ferrobase’s defining cross-cutting concern. The active Tenant for a request is stored in a ContextVar defined in context, set by TenantMiddleware on the way in and read by both synchronous views and asynchronous Channels consumers without any explicit argument passing. ORM queries are dispatched by TenantRouter to the matching database alias; provisioning — PostgreSQL role and database creation, migration application, fixture loading — lives in tenant_mgm. The result is that downstream apps write ordinary Django code and the tenant boundary is enforced underneath them.

Cross-cutting infrastructure

A handful of platform utilities turn the domain models into a running system, and stay open as extension points for other apps:

  • The DI container in containers (built with dependency_injector) is initialised in FerrobaseConfig.ready, wired into the cheapyq task queue and selected services, and parameterised through the FERROBASE_CONTAINER settings block.

  • The shared HTTPX client is managed by context as an ASGI lifespan manager so every outbound HTTP call — most prominently the ZITADEL adapter — reuses a single connection pool.

  • The frontend update channel. FrontendUIUpdater serialises Element / UpdateSpec trees and broadcasts them over a Django Channels group, so server-side state changes repaint UI fragments live without a full page reload.

  • Generic CRUD. generic defines the platform’s gold-standard class-based views — CreateView, UpdateView, DeleteView, ListView, FilterView, TableView — paired with model_crud_routes to mint URL patterns from them. Other apps build their CRUD on top, not on raw Django.

  • Menu registration. SettingsMenuRegistry collects MenuItem contributions from every installed app’s AppConfig so the settings navigation is composed at startup rather than hard-coded.

Package layout

  • models — ORM models grouped by concern: base (UUID model and the mixin family AccessKey, Address, Coordinates, ReadOnly), identity (organisations, tenants, users, API tokens), subscription (plans, features, assignments), file (stored files, access grants), shared (countries, languages, materials, partners, sites, ID sequences, menu items), registry (lookup-table back ends), plus decimal, fields, units, and builtin.

  • services — business logic: the ZITADEL IAM adapter, tenant provisioning, user management, onboarding, scientific unit conversion, the cheapyq task-queue glue, image CAPTCHA, password-reset email dispatch, Server-Sent Events client, static-file serving, cleanup tasks, and platform updaters.

  • views — the generic CRUD framework plus per-domain views for users, organisations, tenants, settings, groups, materials, partners, files, API tokens, and the dashboard index.

  • middleware — request-pipeline middleware: tenant (per-request routing plus TenantRouter), cache, language, asgi, and appmeta.

  • tenant — the active-tenant ContextVar, tenant-scoped query helpers, and the forms / tables that surface tenants in the UI.

  • api — base DRF infrastructure (exception handlers, router, base serializers, base viewsets) that the app-level REST APIs build on.

  • auth — role definitions used by the permission system.

  • forms — base form mixins (copy, fields, model_forms).

  • session — password-reset session forms and templated email dispatch.

  • tables — reusable django-tables2 columns and base tables with platform rendering for currency, units, dates, arrays, inline edit, and summing footers.

  • templatetags — template helpers spanning Bootstrap 5 widgets, datetime / decimal formatting, IAM checks, menu rendering, and the AJAX / HTMX loading-indicator glue.

  • management — Django management commands, including ferrobase_update, the umbrella updater other apps hook into.

Top-level modules cover cross-cutting concerns:

  • apps — Django AppConfig that builds the DI container, registers the HTTPX lifespan manager, and exposes the AppMeta, MenuItem, and SettingsMenuRegistry extension types other apps use.

  • containersdependency_injector Container wiring.

  • consumers — Django Channels WebSocket consumers used by the frontend update channel.

  • context — ASGI lifespan context manager for the shared HTTPX client.

  • context_processors — Django template context processors.

  • encodings — character-encoding detection and the catalogue of supported text encodings.

  • filter — base django-filter FilterSet definitions reused across apps.

  • frontend — primitives for pushing real-time UI updates over Channels.

  • templating — utilities for rendering Django templates from raw strings.

  • urls — root URL configuration for the ferrobase application.

External integrations

Ferrobase carries the platform’s interfaces to the outside world so downstream apps don’t have to:

  • ZITADEL. The IAM provider behind every authenticated request. zitadel wraps the ZITADEL IAM v2 REST API for user, organisation, and project management; end users authenticate over ZITADEL’s OIDC flows and are mirrored into ferrobase’s User records.

  • PostgreSQL multi-tenancy. Tenant provisioning in tenant_mgm creates one PostgreSQL role and database per tenant and runs migrations and fixtures against it; TenantRouter dispatches each ORM query to the right alias at runtime.

  • Django Channels over Redis / Valkey. The channel layer carries the frontend update broadcasts and any other WebSocket traffic served by consumers.

  • HTTPX. All outbound HTTP — ZITADEL, file storage backends, third-party APIs called from downstream apps — flows through the shared connection pool managed by the lifespan context manager in context.

Subpackages

Submodules

ferrosoft.apps.ferrobase.apps module

Django application configuration, menu registration types, and platform app metadata.

class ferrosoft.apps.ferrobase.apps.AppMeta(title, app_name, home_view, icon_path, restricted_app_menu=False, templated_icon=False)[source]

Bases: object

AppMeta provides information about platform applications.

app_name: str

Django identifier of the application.

home_view: str

Django view name of the home page.

icon_path: str

File path to app icon. It must be a static file if templated_icon = False, otherwise a template file.

restricted_app_menu: bool = False

Show to the user only the menu of the app, even if the user navigates to a different app.

templated_icon: bool = False

If true, the icon is {% include %}’d in the template (useful for SVG).

title: Any

Human readable title of the application.

class ferrosoft.apps.ferrobase.apps.FerrobaseConfig(app_name, app_module)[source]

Bases: AppConfig

Django AppConfig for the ferrobase application.

Initialises the dependency injection container on startup and registers the HTTPX lifespan manager so a single connection pool is shared across all inbound requests.

container = None
default_auto_field = 'django.db.models.BigAutoField'
name = 'ferrosoft.apps.ferrobase'
ready()[source]

Wire up the DI container and register the HTTPX lifespan manager.

verbose_name = 'Ferrosoft Base'
class ferrosoft.apps.ferrobase.apps.MenuItem(display_name, view_name, permissions=<factory>, category_name=None, icon=None)[source]

Bases: object

A single entry in a navigation or settings menu.

category_name: str | Any | None = None
display_name: str | Any
icon: str | None = None
permissions: list
view_name: str
class ferrosoft.apps.ferrobase.apps.SettingsMenuRegistry[source]

Bases: object

Registry of settings-page menu items with permission-based filtering.

ITEMS = [MenuItem(display_name='My settings', view_name='ferrobase:my_settings', permissions=[], category_name='', icon=None), MenuItem(display_name='Account Security', view_name='two_factor:profile', permissions=[], category_name='', icon=None), MenuItem(display_name='Business partners', view_name='ferrobase:businesspartner_index', permissions=['ferrobase.view_businesspartner'], category_name='Core Data', icon=None), MenuItem(display_name='API Tokens', view_name='ferrobase:apitoken_index', permissions=['ferrobase.view_apitoken'], category_name='System', icon=None), MenuItem(display_name='ID Sequences', view_name='ferrobase:idsequenceconfig_index', permissions=['ferrobase.view_idsequenceconfig'], category_name='System', icon=None), MenuItem(display_name='Material', view_name='ferrobase:material_index', permissions=['ferrobase.view_material'], category_name='Core Data', icon=None), MenuItem(display_name='Company Sites', view_name='ferrobase:companysite_index', permissions=['ferrobase.view_companysite'], category_name='Core Data', icon=None), MenuItem(display_name='Users', view_name='ferrobase:user_index', permissions=['ferrobase.view_user'], category_name='System', icon=None), MenuItem(display_name='Groups', view_name='ferrobase:group_index', permissions=['auth.view_group'], category_name='System', icon=None), MenuItem(display_name='Organizations', view_name='ferrobase:organization_index', permissions=['ferrobase.view_organization'], category_name='System', icon=None), MenuItem(display_name='Subscription Plans', view_name='ferrobase:subscriptionplan_index', permissions=['ferrobase.view_subscriptionplan'], category_name='System', icon=None), MenuItem(display_name='Files', view_name='ferrobase:directory_root', permissions=['ferrobase.view_directory'], category_name='', icon=None), MenuItem(display_name='Emitters', view_name='emiflow:emitter_index', permissions=['ferrobase.view_material'], category_name='Emissions', icon=None), MenuItem(display_name='Transport Defaults', view_name='emiflow:settings_transport_defaults', permissions=['emiflow.view_transportchaindefaults'], category_name='Emissions', icon=None), MenuItem(display_name='Lifecycle Categories', view_name='emiflow:lifecyclecategory_index', permissions=['emiflow.view_lifecyclecategory'], category_name='Emissions', icon=None), MenuItem(display_name='Energy Carriers', view_name='emiflow:energycarrier_index', permissions=['emiflow.view_energycarrier'], category_name='Emissions', icon=None)]
classmethod add_items(*items)[source]

Add menu items to the registry.

Parameters:

*items (MenuItem) – MenuItem instances to append to the shared registry.

classmethod get_menu(request, items=None)[source]

Return settings menu items the requesting user is permitted to see.

Parameters:
  • request (HttpRequest) – The current HTTP request; its user is checked for each item’s required permissions.

  • items (list[MenuItem] | None) – Candidate menu items to filter. Defaults to the class-level registry when None.

Returns:

Permitted items sorted by category then display

name.

Return type:

Iterable[MenuItem]

ferrosoft.apps.ferrobase.consumers module

Django Channels WebSocket consumers for real-time frontend UI updates.

exception ferrosoft.apps.ferrobase.consumers.EntityIdRequired[source]

Bases: Exception

Raised when a WebSocket consumer requires an entity ID that was not provided.

class ferrosoft.apps.ferrobase.consumers.UIEventsConsumer(updater_class, *args, **kwargs)[source]

Bases: JsonWebsocketConsumer

WebSocket consumer that dispatches UI update events to the frontend.

Subclasses must implement on_initial_ui() to push the initial UI state when a client connects. All incoming messages are processed inside the active tenant context resolved from the connection scope.

connect()[source]

Resolve the optional entity ID, create the updater, and accept the connection.

disconnect(code)[source]

Leave the channel group when the WebSocket connection closes.

frontend_update(event)[source]

Forward a channel-layer group message to the WebSocket client.

on_initial_ui()[source]

Push the initial UI state to a newly connected client.

Subclasses must override this method and call self.send_json with the appropriate payload.

receive_json(content, **kwargs)[source]

Handle an incoming JSON message within the active tenant context.

Sends a ui-failed response and logs exceptions if processing fails.

receive_json_tenant_context(content)[source]

Process a JSON message once the tenant context is active.

send_ui_failed(message=None)[source]

Send a ui-failed transition message to the connected client.

Parameters:

message (str | None) – Optional human-readable error detail.

ferrosoft.apps.ferrobase.containers module

class ferrosoft.apps.ferrobase.containers.Container(**overriding_providers)[source]

Bases: DeclarativeContainer

Ferrobase dependency injection container.

Provides a shared HTTPX AsyncClient and a factory for the cheapyq task queue. Configuration is injected from Django settings via the FERROBASE_CONTAINER key inside AppConfig.ready.

cls_providers = {'config': <dependency_injector.providers.Configuration('config')>, 'httpx_client': <dependency_injector.providers.Singleton(<class 'httpx.AsyncClient'>)>, 'task_queue': <dependency_injector.providers.Factory(<class 'ferrosoft.apps.ferrobase.services.taskqueue.CheapyTaskQueue'>)>}
config = <dependency_injector.providers.Configuration('config')>
containers = {}
httpx_client = <dependency_injector.providers.Singleton(<class 'httpx.AsyncClient'>)>
inherited_providers = {}
providers = {'__self__': <dependency_injector.providers.Self(<class 'ferrosoft.apps.ferrobase.containers.Container'>)>, 'config': <dependency_injector.providers.Configuration('config')>, 'httpx_client': <dependency_injector.providers.Singleton(<class 'httpx.AsyncClient'>)>, 'task_queue': <dependency_injector.providers.Factory(<class 'ferrosoft.apps.ferrobase.services.taskqueue.CheapyTaskQueue'>)>}
task_queue = <dependency_injector.providers.Factory(<class 'ferrosoft.apps.ferrobase.services.taskqueue.CheapyTaskQueue'>)>
wiring_config = <dependency_injector.containers.WiringConfiguration object>

ferrosoft.apps.ferrobase.context module

ASGI lifespan context managers for shared HTTP client lifecycle.

ferrosoft.apps.ferrobase.context.httpx_lifespan_manager()[source]

Manage an HTTPX AsyncClient across the full ASGI application lifespan.

Yields a state dict so that request handlers can reuse one long-lived connection pool via request.state.httpx_client instead of opening a new connection per request.

Yields:

dict{"httpx_client": httpx.AsyncClient}

ferrosoft.apps.ferrobase.context_processors module

Django template context processors for ferrobase.

ferrosoft.apps.ferrobase.context_processors.ferrobase(request)[source]

Inject ferrobase-wide variables into every Django template context.

Parameters:

request – The current HTTP request.

Returns:

Context variables covering the active tenant, colour theme,

contact email, map config, debug flag, login URL, and subscription plan URL.

Return type:

dict

ferrosoft.apps.ferrobase.encodings module

Character-encoding detection and the catalogue of supported text encodings.

class ferrosoft.apps.ferrobase.encodings.Encoding(name, aliases, languages)[source]

Bases: object

Descriptor for a single character encoding.

aliases: List[str]
languages: List[str]
name: str
class ferrosoft.apps.ferrobase.encodings.SupportedEncodings[source]

Bases: object

Catalogue of character encodings offered as user-selectable form choices.

classmethod choices()[source]

Return Django-style (value, label) tuples for all supported encodings.

Returns:

Pairs of (encoding_name, human_readable_label).

Return type:

list[tuple[str, str]]

ferrosoft.apps.ferrobase.encodings.detect_text_encoding(file)[source]

Detect the character encoding of a binary file-like object.

Reads up to _DETECTOR_BUFFER_SIZE lines using chardet’s universal detector and stops early when confidence is high enough.

Parameters:

file – A binary file-like object to inspect.

Returns:

The detected encoding name (e.g. "utf-8").

Return type:

str

ferrosoft.apps.ferrobase.filter module

Django-filter FilterSet definitions for ferrobase models.

class ferrosoft.apps.ferrobase.filter.APITokenFilterSet(data=None, queryset=None, *, request=None, prefix=None)[source]

Bases: FilterSet

Filter API tokens by name substring or exact status.

class Meta[source]

Bases: object

fields = {'name': ['icontains'], 'status': ['exact']}
model

alias of APIToken

base_filters = {'name__icontains': <django_filters.filters.CharFilter object>, 'status': <django_filters.filters.ChoiceFilter object>}
declared_filters = {}
class ferrosoft.apps.ferrobase.filter.CompanySiteFilterSet(data=None, queryset=None, *, request=None, prefix=None)[source]

Bases: FilterSet

Filter company sites by code, name, city, street, postal code, or country.

class Meta[source]

Bases: object

fields = {'city': ['icontains'], 'code': ['istartswith'], 'country': ['exact'], 'name': ['icontains'], 'postal_code': ['istartswith'], 'street': ['icontains']}
model

alias of CompanySite

base_filters = {'city__icontains': <django_filters.filters.CharFilter object>, 'code__istartswith': <django_filters.filters.CharFilter object>, 'country': <django_filters.filters.ModelChoiceFilter object>, 'name__icontains': <django_filters.filters.CharFilter object>, 'postal_code__istartswith': <django_filters.filters.CharFilter object>, 'street__icontains': <django_filters.filters.CharFilter object>}
declared_filters = {}
class ferrosoft.apps.ferrobase.filter.IDSequenceFilter(data=None, queryset=None, *, request=None, prefix=None)[source]

Bases: FilterSet

Filter ID sequence configurations by name or prefix.

class Meta[source]

Bases: object

fields = ['name', 'prefix']
model

alias of IdSequenceConfig

base_filters = {'name': <django_filters.filters.ChoiceFilter object>, 'prefix': <django_filters.filters.CharFilter object>}
declared_filters = {}
class ferrosoft.apps.ferrobase.filter.MaterialFilterSet(data=None, queryset=None, *, request=None, prefix=None)[source]

Bases: FilterSet

Filter materials by code prefix or name substring.

class Meta[source]

Bases: object

fields = {'code': ['istartswith'], 'name': ['icontains']}
model

alias of Material

base_filters = {'code__istartswith': <django_filters.filters.CharFilter object>, 'name__icontains': <django_filters.filters.CharFilter object>}
declared_filters = {}
class ferrosoft.apps.ferrobase.filter.OrganizationFilterSet(data=None, queryset=None, *, request=None, prefix=None)[source]

Bases: FilterSet

Filter organisations by name, contact email, or contact name.

class Meta[source]

Bases: object

fields = {'contact_email': ['icontains'], 'contact_first_name': ['icontains'], 'contact_last_name': ['icontains'], 'name': ['istartswith']}
model

alias of Organization

base_filters = {'contact_email__icontains': <django_filters.filters.CharFilter object>, 'contact_first_name__icontains': <django_filters.filters.CharFilter object>, 'contact_last_name__icontains': <django_filters.filters.CharFilter object>, 'name__istartswith': <django_filters.filters.CharFilter object>}
declared_filters = {}
class ferrosoft.apps.ferrobase.filter.StoredFileFilterSet(data=None, queryset=None, *, request=None, prefix=None)[source]

Bases: FilterSet

Filter stored files by name substring.

class Meta[source]

Bases: object

fields = {'name': ['icontains']}
model

alias of StoredFile

base_filters = {'name__icontains': <django_filters.filters.CharFilter object>}
declared_filters = {}
class ferrosoft.apps.ferrobase.filter.SubscriptionPlanFilterSet(data=None, queryset=None, *, request=None, prefix=None)[source]

Bases: FilterSet

Filter subscription plans by name or maximum tonnage range.

class Meta[source]

Bases: object

fields = ['name', 'max_tonnage']
model

alias of SubscriptionPlan

base_filters = {'max_tonnage': <django_filters.filters.RangeFilter object>, 'name': <django_filters.filters.CharFilter object>}
declared_filters = {'max_tonnage': <django_filters.filters.RangeFilter object>}
class ferrosoft.apps.ferrobase.filter.UserFilterSet(data=None, queryset=None, *, request=None, prefix=None)[source]

Bases: FilterSet

Filter users by username, email address, or organisation.

class Meta[source]

Bases: object

fields = ['username', 'email', 'organization']
model

alias of User

base_filters = {'email': <django_filters.filters.CharFilter object>, 'organization': <django_filters.filters.ModelChoiceFilter object>, 'username': <django_filters.filters.CharFilter object>}
declared_filters = {}

ferrosoft.apps.ferrobase.frontend module

Primitives for pushing real-time UI updates over WebSocket to the frontend.

Provides a hierarchy of serialisable element types (Element, UpdateSpec, etc.) and FrontendUIUpdater, which broadcasts update specs to a Django Channels channel group so connected clients can apply them without a full page reload.

class ferrosoft.apps.ferrobase.frontend.AppendSpec(selector, elements)[source]

Bases: object

Specification for appending new elements to a CSS-selector target.

class ferrosoft.apps.ferrobase.frontend.AppendSpecSerializer(*args, **kwargs)[source]

Bases: Serializer

DRF serialiser for AppendSpec.

class ferrosoft.apps.ferrobase.frontend.Attr(name, value=None)[source]

Bases: object

Represents HTML element attribute.

class ferrosoft.apps.ferrobase.frontend.DeleteSpec(selector)[source]

Bases: object

Specification for removing all elements matching a CSS selector.

class ferrosoft.apps.ferrobase.frontend.DeleteSpecSerializer(*args, **kwargs)[source]

Bases: Serializer

DRF serialiser for DeleteSpec.

class ferrosoft.apps.ferrobase.frontend.Element(name, *args, **kwargs)[source]

Bases: object

Element represents HTML elements or XML elements of certain namespace (i.e. SVG).

class ferrosoft.apps.ferrobase.frontend.ElementSerializer(*args, **kwargs)[source]

Bases: Serializer

DRF serialiser for Element that outputs camelCase JSON for the frontend.

static get_children(obj)[source]
to_representation(instance)[source]

Object instance -> Dict of primitive datatypes.

class ferrosoft.apps.ferrobase.frontend.FrontendUIUpdater(*args, **kwargs)[source]

Bases: object

Update frontend UI with new or replacing elements.

property group_name

Django Channels group name this updater broadcasts to.

update(spec, transition=None)[source]

Broadcast an update specification to all clients in the channel group.

Parameters:
  • spec (UpdateSpec) – The UI update to apply. Elements targeted by replace_children must carry an id attribute.

  • transition (Transition | None) – Optional named transition to accompany the update.

class ferrosoft.apps.ferrobase.frontend.Namespace(*values)[source]

Bases: StrEnum

XML/HTML namespace identifiers used when serialising mixed-namespace elements.

HTML = 'html'
SVG = 'http://www.w3.org/2000/svg'
class ferrosoft.apps.ferrobase.frontend.Position(x, y)

Bases: tuple

A 2D coordinate used to position UI elements (x, y).

x

Alias for field number 0

y

Alias for field number 1

class ferrosoft.apps.ferrobase.frontend.Transition(transition, detail=None)[source]

Bases: object

A named UI transition with an optional detail payload.

class ferrosoft.apps.ferrobase.frontend.TransitionSerializer(*args, **kwargs)[source]

Bases: Serializer

DRF serialiser for Transition.

class ferrosoft.apps.ferrobase.frontend.UpdateSpec(replace_children=None, append_children=None, delete_elements=None)[source]

Bases: object

Aggregate specification describing a complete frontend UI update.

Combines element replacements, appends, and deletions into a single payload that FrontendUIUpdater.update broadcasts to connected clients.

class ferrosoft.apps.ferrobase.frontend.UpdateSpecSerializer(*args, **kwargs)[source]

Bases: Serializer

DRF serialiser for UpdateSpec that outputs camelCase JSON.

static get_append_children(obj)[source]
static get_delete_elements(obj)[source]
static get_replace_children(obj)[source]
to_representation(instance)[source]

Object instance -> Dict of primitive datatypes.

ferrosoft.apps.ferrobase.templating module

Utilities for rendering Django templates from raw strings.

ferrosoft.apps.ferrobase.templating.template_from_string(template_string, using=None)[source]

Convert a string into a template object, using a given template engine or using the default backends from settings.TEMPLATES if no engine was specified.

ferrosoft.apps.ferrobase.urls module

URL configuration for the ferrobase application.