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
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
- You need to create a custom field model and a custom value model.
- Your custom field model should inherit from
django_features.custom_fields.models.field.AbstractBaseCustomField. - 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_fieldsapp by setting theCUSTOM_FIELD_MODELorCUSTOM_FIELD_VALUE_MODELsetting. - The swapped models should inherit from
django_features.custom_fields.models.field.AbstractBaseCustomFieldordjango_features.custom_fields.models.value.AbstractBaseCustomValue.
Models with custom values
- Your models with custom values should inherit from
django_features.custom_fields.models.CustomFieldBaseModel. - 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)
| File | Size | Uploaded | |
|---|---|---|---|
| ftw_django_features-2026.5.0.dev0.tar.gz | 38.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|