django_utils.admin package

Submodules

django_utils.admin.export module

Streaming CSV/JSON export actions for the admin.

Dependency-free (stdlib csv/json), streaming (StreamingHttpResponse + queryset_iterator, so a million-row export never materialises in memory), and scoped on purpose: CSV and JSON only, export only. For XLSX, import flows, resource classes and the rest, use django-import-export -- this module exists for the 90% case without its weight.

CSV injection: spreadsheet applications execute cell values starting with =, +, -, @, a tab (\t) or a carriage return (\r) as formulas (OWASP's full dangerous-prefix list -- the last two are less obvious than the arithmetic operators since some CSV parsers strip leading whitespace before a spreadsheet application ever sees the cell, but not all do). Exported text values starting with any of those characters are prefixed with a single quote (documented OWASP mitigation); numbers and other non-str values are left untouched.

Both actions run against the queryset the changelist hands them -- already narrowed by list_filter, search, and this package's own admin filters (django_utils.admin.filters) -- so the intended flow is filter first, select second, export third: the export never sees a row the changelist wouldn't have shown.

class django_utils.admin.export.ExportMixin[source]

Bases: object

Adds export_as_csv/export_as_json admin actions.

Both stream via StreamingHttpResponse: rows are pulled one at a time from django_utils.queryset.queryset_iterator, so exporting a million rows never materialises the table in memory. They run against whatever queryset the changelist hands them -- see the module docstring for the filter-then-export composition story.

export_fields picks the exported columns; left unset, it defaults to every concrete field's attname (tuple(f.attname for f in model._meta.concrete_fields)), so foreign keys export as <name>_id values rather than related objects:

class IngredientAdmin(ExportMixin, admin.ModelAdmin):
    export_fields = ('name', 'stock')

actions = ('export_as_csv', 'export_as_json') is a plain class attribute here, picked up by ModelAdmin's default get_actions() -- nothing to override. An admin that declares its own actions list replaces this one outright (Python attribute lookup, not a merge), so include both names in it:

class IngredientAdmin(ExportMixin, admin.ModelAdmin):
    actions = ('export_as_csv', 'export_as_json', 'my_action')
actions = ('export_as_csv', 'export_as_json')
export_as_csv(request: HttpRequest, queryset: QuerySet[Any]) StreamingHttpResponse[source]
export_as_json(request: HttpRequest, queryset: QuerySet[Any]) StreamingHttpResponse[source]
export_fields: tuple[str, ...] | None = None

django_utils.admin.filters module

class django_utils.admin.filters.AllValuesFieldListFilter(field, request, params, model, model_admin, field_path)[source]

Bases: FieldListFilter

choices(changelist)[source]

Return choices ready to be output in the template.

changelist is the ChangeList to be displayed.

expected_parameters()[source]

Return the list of parameter names that are expected from the request's query string and that will be used by this filter.

get_facet_counts(pk_attname, filtered_qs)[source]
class django_utils.admin.filters.BooleanFieldListFilter(field, request, params, model, model_admin, field_path)[source]

Bases: FieldListFilter

choices(changelist)[source]

Return choices ready to be output in the template.

changelist is the ChangeList to be displayed.

expected_parameters()[source]

Return the list of parameter names that are expected from the request's query string and that will be used by this filter.

get_facet_counts(pk_attname, filtered_qs)[source]
class django_utils.admin.filters.ChoicesFieldListFilter(field, request, params, model, model_admin, field_path)[source]

Bases: FieldListFilter

choices(changelist)[source]

Return choices ready to be output in the template.

changelist is the ChangeList to be displayed.

expected_parameters()[source]

Return the list of parameter names that are expected from the request's query string and that will be used by this filter.

get_facet_counts(pk_attname, filtered_qs)[source]
class django_utils.admin.filters.DateFieldListFilter(field, request, params, model, model_admin, field_path)[source]

Bases: FieldListFilter

choices(changelist)[source]

Return choices ready to be output in the template.

changelist is the ChangeList to be displayed.

expected_parameters()[source]

Return the list of parameter names that are expected from the request's query string and that will be used by this filter.

get_facet_counts(pk_attname, filtered_qs)[source]
class django_utils.admin.filters.FieldListFilter(field, request, params, model, model_admin, field_path)[source]

Bases: FacetsMixin, ListFilter

classmethod create(field, request, params, model, model_admin, field_path)[source]
has_output()[source]

Return True if some choices would be output for this filter.

list_separator = ','
queryset(request, queryset)[source]

Return the filtered queryset.

classmethod register(test, list_filter_class, take_priority=False)[source]
class django_utils.admin.filters.ListFilter(request, params, model, model_admin)[source]

Bases: object

choices(changelist)[source]

Return choices ready to be output in the template.

changelist is the ChangeList to be displayed.

expected_parameters()[source]

Return the list of parameter names that are expected from the request's query string and that will be used by this filter.

has_output()[source]

Return True if some choices would be output for this filter.

queryset(request, queryset)[source]

Return the filtered queryset.

template = 'admin/filter.html'
title = None
class django_utils.admin.filters.RelatedFieldListFilter(field, request, params, model, model_admin, field_path)[source]

Bases: FieldListFilter

choices(changelist)[source]

Return choices ready to be output in the template.

changelist is the ChangeList to be displayed.

expected_parameters()[source]

Return the list of parameter names that are expected from the request's query string and that will be used by this filter.

field_admin_ordering(field, request, model_admin)[source]

Return the model admin's ordering for related field, if provided.

field_choices(field, request, model_admin)[source]
get_facet_counts(pk_attname, filtered_qs)[source]
has_output()[source]

Return True if some choices would be output for this filter.

property include_empty_choice

Return True if a "(None)" choice should be included, which filters out everything except empty relationships.

class django_utils.admin.filters.RelatedOnlyFieldListFilter(field, request, params, model, model_admin, field_path)[source]

Bases: RelatedFieldListFilter

field_choices(field, request, model_admin)[source]
class django_utils.admin.filters.SimpleListFilter(request, params, model, model_admin)[source]

Bases: FacetsMixin, ListFilter

choices(changelist)[source]

Return choices ready to be output in the template.

changelist is the ChangeList to be displayed.

expected_parameters()[source]

Return the list of parameter names that are expected from the request's query string and that will be used by this filter.

get_facet_counts(pk_attname, filtered_qs)[source]
has_output()[source]

Return True if some choices would be output for this filter.

lookups(request, model_admin)[source]

Must be overridden to return a list of tuples (value, verbose value)

parameter_name = None
value()[source]

Return the value (in string format) provided in the request's query string for this filter, if any, or None if the value wasn't provided.

django_utils.admin.mixins module

Opt-in ModelAdmin mixins.

ReadOnlyModelAdminMixin turns any existing admin, including one using this package's filters and widgets, into a safe read-only view: add/change/delete are denied for everyone (superusers included), every concrete field and many-to-many relation becomes read-only, and the list configuration (list_display, list_filter, search_fields) keeps working untouched. Built exclusively on stable public ModelAdmin API.

CountColumnMixin adds sortable list_display columns for related object counts (count_columns = ('review',) gives a review_count column), annotated via django_utils.aggregates.SubqueryCount so combining several relations never fans out through a JOIN.

class django_utils.admin.mixins.CountColumnMixin(*args: Any, **kwargs: Any)[source]

Bases: object

Sortable related-object count columns, without JOIN fan-out.

count_columns: tuple[str, ...] = ()
get_list_display(request: HttpRequest) tuple[Any, ...][source]
get_queryset(request: HttpRequest) Any[source]
class django_utils.admin.mixins.ReadOnlyModelAdminMixin[source]

Bases: object

Deny add/change/delete; make every field read-only.

Governs the parent admin's own permissions only: an editable inlines entry on the wrapped admin is not made read-only by this mixin. Django still denies the write: the change view's POST path is gated on the parent admin's has_change_permission, which this mixin hard-denies -- so nothing is actually editable through it. But the change-form UI still renders its widgets as if it were, which can mislead a user into thinking edits are possible. Apply this mixin to inline admin classes too if you want their rendered UI to match.

get_readonly_fields(request: HttpRequest, obj: Any = None) tuple[str, ...][source]
has_add_permission(request: HttpRequest) bool[source]
has_change_permission(request: HttpRequest, obj: Any = None) bool[source]
has_delete_permission(request: HttpRequest, obj: Any = None) bool[source]
model: Any

django_utils.admin.widgets module

Admin widgets for working with JSONField.

class django_utils.admin.widgets.JSONWidget(attrs: dict[str, Any] | None = None)[source]

Bases: Textarea

A JSONField textarea that pretty-prints and validates client-side.

Django renders a stored value on a single line ({"b": 2, "a": [1, 2]}) and reports parse errors only after a submit round-trip. This widget indents and key-sorts the value, and reports parse errors inline as you type.

Malformed input is not reformatted: Django already round-trips it through InvalidJSONInput and that behaviour is preserved here.

class Media[source]

Bases: object

css: ClassVar[dict[str, list[str]]] = {'all': ['django_utils/admin/json_widget.css']}
js: ClassVar[list[str]] = ['django_utils/admin/json_widget.js']
format_value(value: Any) Any[source]

Return a value as it should appear when rendered in a template.

property media
class django_utils.admin.widgets.JSONWidgetMixin[source]

Bases: object

Opt a ModelAdmin into JSONWidget for its JSON fields.

class MyAdmin(JSONWidgetMixin, admin.ModelAdmin):
    pass

Nothing is patched globally: a project that installs this package for the filters alone sees no change to its forms.

This works by declaring a class-level formfield_overrides and relies on normal Python attribute lookup to make it visible on the final class, so it is a silent no-op in two situations:

  • The ModelAdmin declares its own formfield_overrides. That dict replaces this mixin's entirely rather than merging with it (regular class-attribute shadowing, not a dict merge), so models.JSONField is no longer mapped to JSONWidget and Django's default JSON textarea is used instead -- no error, no warning.

  • The mixin is listed after admin.ModelAdmin in the class's bases, e.g. class MyAdmin(admin.ModelAdmin, JSONWidgetMixin). admin.ModelAdmin already defines formfield_overrides (as an empty dict), so MRO resolves the attribute there first and this mixin's mapping is never consulted.

If your ModelAdmin needs its own formfield_overrides for other fields, merge this mixin's formfield_overrides mapping in explicitly instead of overriding it outright, and keep this mixin first in the base list.

formfield_overrides: ClassVar[dict[type[models.Field[Any, Any]], Any]] = {<class 'django.db.models.fields.json.JSONField'>: {'widget': <class 'django_utils.admin.widgets.JSONWidget'>}}

Module contents