Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

django-features

A collection of fearures used in our Django-based web applications

Changelog

Installation

pip install ftw-django-features

Usage

Add desired app to INSTALLED_APPS in your Django project.

Available apps:

django_features.system_message
django_features.custom_fields

Configuration

If you want to use django_features, your base configuration class should inherit from django_features.settings.BaseConfiguration.

from django_features.settings import BaseConfiguration


class Base(BaseConfiguration):
    ...

Custom Fields

To use all features of the django_features.custom_fields app, the following steps are required:

Add the django_features.custom_fields.routers.custom_field_router to your ROOT_URLCONF. For example:

path("api/", include(custom_field_router.urls)),

Create your own custom field and value models

  1. You need to create a custom field model and a custom value model.
  2. Your custom field model should inherit from django_features.custom_fields.models.field.AbstractBaseCustomField.
  3. Your custom value model should inherit from django_features.custom_fields.models.value.AbstractBaseCustomValue.

Configuration

  • You can configure the models used by the django_features.custom_fields app by setting the CUSTOM_FIELD_MODEL or CUSTOM_FIELD_VALUE_MODEL setting.
  • The swapped models should inherit from django_features.custom_fields.models.field.AbstractBaseCustomField or django_features.custom_fields.models.value.AbstractBaseCustomValue.

Models with custom values

  1. Your models with custom values should inherit from django_features.custom_fields.models.CustomFieldBaseModel.
  2. Your models should have a relation to the custom value model. For example:
    • custom_values = models.ManyToManyField(blank=True, to=CustomValue, verbose_name=_("Benutzerdefinierte Werte"))

Querysets

Your querysets for the models with custom values should inherit from django_features.custom_fields.models.CustomFieldModelBaseManager.

Serializers

Your serializers for the models with custom values should inherit from django_features.custom_fields.serializers.CustomFieldBaseModelSerializer.

System Message

If you want to use django_features.system_message, your base configuration class should inherit from django_features.system_message.settings.SystemMessageConfigurationMixin.

Then call the super property:

@property
def CONSTANCE_CONFIG(self) -> dict:
    config = super().CONSTANCE_CONFIG
    return {**config, ...}

@property
def CONSTANCE_CONFIG_FIELDSETS(self) -> dict:
    config = super().CONSTANCE_CONFIG_FIELDSETS
    return {
        **config,
        ...
    }

Add the django_features.system_message.routers.system_message_router to your ROOT_URLCONF. For example:

path("api/", include(system_message_router.urls)),

Development

Installing dependencies, assuming you have poetry installed:

poetry install

Release

This package uses towncrier to manage the changelog, and to introduce new changes, a file with a concise title and a brief explanation of what the change accomplishes should be created in the changes directory, with a suffix indicating whether the change is a feature, bugfix, or other.

To make a release and publish it to PyPI, the following command can be executed:

./bin/release

This script utilizes zest.releaser and towncrier to create the release, build the wheel, and publish it to PyPI.

Before running the release command, it is necessary to configure poetry with an access token for PyPI by executing the following command and inserting the token stored in 1password:

poetry config pypi-token.pypi <token>

The version attribute in the pyproject.toml file should be updated to the new version before running the release command, because this version will be published to PyPI.

Custom-field validation and matching contracts

Field metadata now includes required, allow_null, allow_blank, and default. Ordinary choices remain exactly {id, label, value}; import metadata is not added to that representation. allow_blank also controls whether multiple fields accept an empty list. allow_null controls the container and scalar-list items. Required fields must be supplied; configured defaults apply only to optional fields. A stored default=None means no default (the model cannot distinguish an explicit null default from an absent one). Falsy defaults such as False, 0, "", and [] are retained. Model clean() / full_clean() and admin forms reject invalid defaults with a validation error on default. Django save() alone does not call clean(); callers saving configuration directly must validate it first. The shared validate_default(choices=None, validate_choices=True) validator also accepts the final surviving choices of an inline formset. An admin managing pending choices can defer their validation with clean(validate_choices=False) and validate them after the formset has been cleaned. Scalar defaults remain strictly validated. Unsaved inline additions do not participate in canonical-ID default matching, but must still be included in mapping and duplicate validation.

At runtime, invalid scalar/list or canonical choice defaults are omitted using DRF's missing-field mechanism, preserving existing values on updates. Metadata exposes no usable default for an invalid configuration. The named django_features.custom_fields logger emits a warning with safe model/PK context and a stable code, without default values or exception details; the application owns logging configuration. A reused serializer field warns at most once, rather than once per row. Explicit submitted values remain strictly validated. Choice defaults are validated into choice objects when an omitted field uses them. Choice defaults always contain canonical IDs, independently of a mapping client's unique_choice_field; explicit client input still uses that lookup key. Mutable defaults are copied for each application, including each item of a bulk serializer. Defaults initialize creates only. Both full updates (PUT) and partial updates (PATCH) preserve omitted custom fields. Pass the existing instance to the serializer before validation; validating as a create and attaching an instance afterwards cannot distinguish generated defaults from explicit input. Nested update callers must likewise bind the nested object's instance before validation; a new nested object still receives defaults, even when its parent is updated. DRF's standard list serializer does not implement bulk updates; custom list update implementations must bind the appropriate child instance for each row. Mapping clients should resolve identity from mapped, validated identity fields, without running mapping or formatters twice. Mapping default_* functions and ordinary serializer defaults retain their existing behavior.

On creates, valid scalar defaults create CustomValue rows, including allowed empty lists. This storage cost is intentional: absence and an explicit empty value remain distinct. Scalar rows are inserted together with bulk_create; choice defaults attach existing choice rows.

ChoiceIdField(field, unique_field="id", choices=None) accepts an ID or an {"id": ID} dictionary for single fields and a list of those forms for multiple fields. Mixed scalar/dictionary lists are supported. IDs expressed as integer strings remain valid. Selections must belong to the configured field, be unique, and match unambiguously. Invalid shapes and lookup values raise DRF ValidationError, with codes including shape, invalid, missing, duplicate, ambiguous, null, blank, and empty. Configuring unique_field="pk" selects the concrete primary key; UUID primary keys also accept native UUID objects. Integer primary keys reject booleans and floats. Alternative concrete lookup fields accept native values supported by that model field, such as dates, datetimes, decimals, and UUIDs, as well as their accepted string forms.

A configured unique_field must be a concrete non-relation choice-model field. Dictionary inputs use that key when present, otherwise id, for both single and multiple choices. The configured key wins when both occur; extra display metadata is ignored. pk and concrete field-name keys require explicit configuration. Lookup values must be hashable and accepted by the configured model field; lists and dictionaries return invalid. Model-field conversion precedes typed indexing, so JSON values 1, true, and "1" remain distinct. BaseMappingSerializer continues to default to unique_choice_field="value". Single validation returns a choice object; multiple validation returns a fresh plain list in model ordering (or supplied collection ordering), independent of selection order. Allowed empty selections return an empty list. Adding, removing, or reordering items in the returned list does not change the cached catalog or later result lists. The choice objects themselves remain shared.

Explicit matching and bounded queries

from django_features.custom_fields.helpers import get_custom_field_model
from django_features.custom_fields.matching import ChoiceMatcher

fields = list(get_custom_field_model().objects.for_model(MyModel).with_choices())
for field in fields:
    if field.choice_field:
        matcher = ChoiceMatcher(
            field.choices,
            attribute="label",
            language="de",
            expected_type=str,
        )
        choice = matcher.resolve("Exact German label")

with_choices() uses the configured value model's actual reverse relationship: it fetches fields and their choices in two queries. field.choices also reuses ordinary reverse-relation prefetch caches. Pass the same materialized fields as MyCustomFieldSerializer(custom_fields=fields) to reuse them for validation; the caller must select the correct model and filters when supplying fields. The model serializer uses this prefetch when input data is supplied to its constructor or root serializer, including nested and list validation. Without input data, or for a read-only serializer, it leaves choice catalogs lazy; model queries for selected output values are separate. After ContentType warmup, automatically loading nonempty field definitions and choices costs two queries for validation, while definitions alone cost one query for reads. This excludes excluded or caller-supplied fields. Direct run_validation calls without constructor/root input retain lazy choice lookup without a constant query guarantee. Each serializer instance performs its own queries; this does not cache catalogs across instances. The metadata viewset always prefetches choices because its output includes the catalog. ChoiceIdField(..., choices=field.choices) also accepts an explicit collection. These collections and indexes are request-local snapshots: recreate them after configuration writes. Multiple-choice results are plain lists and do not support QuerySet operations.

ChoiceMatcher(choices, *, attribute, language=None, expected_type=None, normalize=None) supports label, value, and external_label. Its input must contain choices from only one field, with the matching columns already loaded. The field_id column must also be loaded; deferred scope metadata is rejected without querying each choice. Label matching requires an explicitly supported translation language and reads that exact column with no fallback. Other attributes are untranslated. Matching is exact, case-sensitive and type-aware; scalar string, integer, float, and boolean keys are distinct. Null/structured keys are rejected by this matching API. Pass expected_type=str to require string-valued configuration and input. Callers may explicitly supply a normalize function to prepare keys and tokens; normalization collisions are rejected. No normalization is implicit. Only finite numbers can be keys; non-finite floats, including normalization output, raise type_mismatch. Normalization type, value, overflow, and recursion failures also become type_mismatch.

An optional/unset external_label is still a literal key in a supplied collection: one blank label matches "", and two blank labels are ambiguous. The matcher does not silently ignore blanks. Callers must explicitly decide whether a field's mapping must be complete or whether to filter out choices before building its index.

resolve(token) returns the original object and resolve_many(tokens) returns a list in token order (including repeated tokens). Once constructed, resolving additional tokens runs no queries. ChoiceMatchError subclasses DRF ValidationError: stable codes are attribute, language, field_scope, type_mismatch, ambiguous, and missing; error text excludes choice values. Duplicates and invalid configuration are rejected while building the index.

Mandatory consumer migration and deployment

AbstractBaseCustomValue now includes an optional untranslated external_label = CharField(max_length=255, blank=True, default=""). Every concrete application model inheriting this abstract model needs its own AddField migration, including swapped/custom value models. Do not redeclare the field locally and do not add it to modeltranslation fields. Existing choice IDs, labels, and JSON values are unchanged; existing rows receive an empty label.

The abstract model has no field foreign key, so it cannot impose field-scoped SQL constraints. Consumers that require unique external labels should add this to their concrete value model's Meta.constraints (adjust the constraint name):

models.UniqueConstraint(
    fields=["field", "external_label"],
    condition=~models.Q(external_label=""),
    name="custom_value_field_external_label_unique",
)

The test consumer demonstrates the inherited field, admin, and constraint in app/custom_field, with migration 0002_add_external_label_constraint and its updated max_migration.txt. In each consuming application, generate a meaningfully named migration, reconcile its migration leaf tracker, review any local Meta overrides, and run migration tests. Deploy the upgraded dependency and consumer migrations together, applying migrations before application workers query the new column. Upgrading the package alone does not update consumer tables.

Candidate and release preparation

pyproject.toml follows the existing version.txt development version 2026.5.0.dev0. It is an unreleased candidate, not a published integration pin. Install GNU gettext (msgfmt on PATH; e.g. apt-get install gettext on Debian/ Ubuntu or brew install gettext on macOS). Compile the translations with python manage.py compilemessages --ignore .venv before building locally with poetry build; compiled catalogs are included in the wheel. Run the focused contracts, the full suite, pytest --migrations app/custom_field/tests, and DJANGO_CONFIGURATION=Testing python manage.py makemigrations --check --dry-run. Use DJANGO_CONFIGURATION=Testing for all tests, plus the isort/flake8/Black commands in .github/workflows/tests.yml. The maintainer must choose the final release version and follow the existing release process above separately; record the actual published version and source revision before pinning it in consumers.

Shared catalogs live in django_features/locale. Installing only the nested django_features.custom_fields app does not make Django discover that directory. Applications must include the shared directory in LOCALE_PATHS or provide the messages in their own discovered catalogs; shipping the compiled catalog alone does not register its location with Django.

Release files for ftw-django-features 2026.5.0.dev0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ftw-django-features 2026.5.0.dev0
File Size Uploaded
ftw_django_features-2026.5.0.dev0.tar.gz 38.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ftw-django-features 2026.5.0.dev0
File Interpreter ABI Platform
ftw_django_features-2026.5.0.dev0-py3-none-any.whl Python 3 none any Details

Total release size: 93.5 kB

Release files / ftw_django_features-2026.5.0.dev0.tar.gz

Download URL ftw_django_features-2026.5.0.dev0.tar.gz
Size 38.8 kB
Tags Source
SHA-256 checksum
How to use checksums
41d7a334c32fc0b8ba68f38936bc4bd91d272408adc95e61c9f893721caecd38
BLAKE2b-256 checksum
How to use checksums
204a1820eec50523d1a12642ac5aa88743f0d1c1379c75cba82ce6c48b6169a9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.4.0 CPython/3.13.13 Darwin/24.6.0

Release files / ftw_django_features-2026.5.0.dev0-py3-none-any.whl

Download URL ftw_django_features-2026.5.0.dev0-py3-none-any.whl
Size 54.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c1b355dd4aac04538435a8dfab870280b0e5dd0836a9ab0de6efc0f0b83b8b66
BLAKE2b-256 checksum
How to use checksums
10be2f39d1a56f6ba6ca035283efb810c2bb926950080378d809a17dab1da0fb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.4.0 CPython/3.13.13 Darwin/24.6.0
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page