This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
django-formdefaults is a Django app shipped on PyPI as django-formdefaults.
Per-user + system-wide database-backed defaults for any Django form, edited
from a popup. Originally extracted from iplweb/bpp.
src/formdefaults/— the published package. Everything inside here ships in the wheel/sdist. Nothing outside is shipped.example_project/— a runnable demo Django site. Excluded from the distribution by[tool.hatch.build.targets.{sdist,wheel}]inpyproject.toml.tests/— pytest suite.tests/settings.pyboots apostgres:16-alpinetestcontainer at import time and stops it viaatexit. Docker must be running.dist/— gitignored build output.Makefile— local-only helper (not shipped).
Three DB models live in src/formdefaults/models.py:
FormRepresentation(PK = dottedfull_nameof the Form class) — one row per registered form. Carries optionalhtml_before/html_afterand apre_registeredflag (True when registered via decorator/setting rather than discovered lazily).FormFieldRepresentation— one row per field of a representation, recordsname,klass(dotted),label,order.FormFieldDefaultValue— the actual default. Either system-wide (user=NULL) or per-user. JSON-serialized viaforms._serialize.is_auto_snapshot=Truemeans "this row mirrorsForm.initialfrom code and may be refreshed automatically". Any UI edit flips it toFalseand freezes the row.
Resolution order at render time (per field): per-user override →
system-wide default → code-level Form.initial. Per-user shadows
system; both shadow code.
Three registration paths, all funnel through
core.update_form_db_repr:
@register_formdecorator (registry.py) — class is registered in a module-level dict, thensignals.snapshot_registered_forms(connected topost_migrate) instantiates each and snapshots its fields.signals.autodiscover_formdefaultsimports<app>.formsfor every installed app first.FORMDEFAULTS_FORMSsetting — list of dotted Form paths, imported and snapshotted the same way.- Lazy / zero-registration —
FormDefaultsMixin(or callingcore.get_form_defaults(form, user)directly from a view) snapshots on first render. The freshness cachecore._LAST_SNAPSHOTkeeps re-snapshots cheap (SNAPSHOT_TTL_SECONDS = 60.0, per-process).
Snapshot reconciliation (core._do_update): keys in Form.fields
get an INSERT … ON CONFLICT upsert into FormFieldRepresentation;
rows for keys that disappeared from code get deleted. klass and
label updates only touch the row if they actually changed.
FormFieldDefaultValue rows with is_auto_snapshot=True are kept in
sync with Form.initial; rows with is_auto_snapshot=False are
preserved verbatim.
Editing UI: server-rendered modal fragment, plain JS
(src/formdefaults/static/formdefaults/modal.{js,css}). No
jQuery/HTMX. views.UserFormDefaultsView (per-user scope) and
views.SystemFormDefaultsView (system scope) both render
templates/formdefaults/_modal_fragment.html. The dynamic edit form
is built by forms.build_user_defaults_form / build_system_defaults_form
— each "real" field gets a paired _override_<name> checkbox; on POST,
unchecked override means "delete the row".
System-wide permission hook (permissions.py): resolution order
per-form formdefaults_can_edit_system_wide attribute →
settings.FORMDEFAULTS_CAN_EDIT_SYSTEM_WIDE (dotted path) →
user.is_superuser.
apps.ready() wires the post_migrate signal and sets
default_auto_field = AutoField (pinned for backwards-compat with
projects whose 0001_initial predates BigAutoField).
pytest # full suite (needs Docker)
pytest tests/test_core.py::test_name # single test
ruff check # lint
ruff check --fix # lint + autofix
ruff format # format
pre-commit run --all-files # both, on all files
make messages # regen both .po files safely
make compilemessages # compile both .mo files
uv build # wheel + sdist into dist/
uv run example_project/manage.py runserver
uv run --extra=example example_project/manage.py run_site # full demo stackTwo separate django.po files live in this repo:
| Path | Scope | Shipped? |
|---|---|---|
src/formdefaults/locale/<lang>/LC_MESSAGES/django.po |
strings used inside src/formdefaults/ |
yes — in the wheel |
example_project/demo/locale/<lang>/LC_MESSAGES/django.po |
demo-only strings | no |
Why it matters. Django merges per-app translations on top of its core
catalog at runtime. If the published package's .po accidentally carries
a generic msgid like "Log in" translated as "Zaloguj", every Django
site that installs formdefaults loses Django core's "Zaloguj się" on
the admin login page. This actually happened pre-0.6.2 (see CHANGELOG).
The rule. Never run django-admin makemessages from the repo root.
xgettext walks everything under cwd, so a root-level run will harvest
demo strings into the package catalog. Always cd into the right scope —
the Makefile does this for you.
make messages LOCALE=de # add a new locale (LOCALE, not LANG — the
# latter clashes with the shell env var)Manual equivalents (if you can't / won't use make):
cd src/formdefaults && django-admin makemessages -l pl --no-obsolete
cd example_project && python manage.py makemessages -l pl --no-obsolete- Bump
versioninpyproject.toml. - Add a section to
CHANGELOG.md(Keep-a-Changelog, semver). make messages compilemessagesif translatable strings changed.- Commit (
chore: release X.Y.Z). git tag -a vX.Y.Z -m "Release X.Y.Z".git push origin main vX.Y.Z.uv build.uv publish— oruv publish dist/django_formdefaults-X.Y.Z*when older versions still linger indist/.
The [example] extra pins run-site>=0.4 because run-site 0.5+ has
been renamed to django-run-site and is not yet on PyPI — PyPI rejects
direct git URLs in extras. Don't "fix" this by adding a git URL.
- Direct pushes to
mainmay be blocked by Claude Code's auto-mode classifier even though the project's history uses them. The user approves interactively when needed. runsite.toml,.dev_helpers_port,.dev_helpers_tokenat the repo root are per-developer files — don't commit them.apps.pypinsdefault_auto_field = AutoFielddeliberately. Don't "modernize" it toBigAutoField— it would surface as a spurious migration on every project that already has this app installed.