Skip to main content

django-crud-views-extensions

Incubator for django-crud-views extension apps. Apps here are named as if they lived in django-crud-views itself (crud_views_widget_*); once mature, an app may move into the main package with no code changes for consuming projects.

Requires django-crud-views >= 0.29.

crud_views_widget_datetimepicker

Form widgets wrapping the xdsoft jQuery DateTimePicker: DateTimePickerInput, DatePickerInput, TimePickerInput.

Prerequisite: jQuery loaded in your base template before {% cv_js %} (same rule as django-crud-views core).

Install

pip install django-crud-views-extensions

INSTALLED_APPS = [
    ...
    "crud_views",
    "crud_views_widget_datetimepicker",
]

That's it — the widget's JS/CSS are delivered through {% cv_js %}/{% cv_css %} on every page (including crud-views modals) via the asset registry. By default the plugin files load from jsDelivr (CDN mode).

Use

from crud_views_widget_datetimepicker.widgets import DateTimePickerInput

class AppointmentForm(CrispyModelForm):
    class Meta:
        model = Appointment
        fields = ["title", "starts_at"]
        widgets = {"starts_at": DateTimePickerInput()}

Options: config={...} (any xdsoft option, e.g. step, minTime), and DateTimePickerInput(fix_year=2026) to lock the picker to one calendar year.

Formats and language

The picker format follows the active request language: the widget takes the first of the locale's DATE_INPUT_FORMATS / DATETIME_INPUT_FORMATS / TIME_INPUT_FORMATS that the picker can reproduce (time and datetime prefer a format without seconds — the picker cannot set them) and renders the value with that same format, so whatever the picker writes is accepted on submit — e.g. 2026-10-15 19:00 under en, 15.10.2026 19:00 under de.

To pin a format, pass it to the picker (config={"format": "d.m.Y"} or settings DEFAULTS) or to Django (DatePickerInput(format="%d.%m.%Y")). The widget renders and parses a pinned format itself, so it works in every locale. Supported tokens: d m Y y H i s; a picker format with other tokens is passed to the picker unchanged and must then be accepted by the field's input_formats.

The picker language is the active request language unless LANG (settings) or config={"lang": ...} sets one. The plugin's language is page-global: one field with its own lang switches every picker on that page.

Settings

CRUD_VIEWS_DATETIMEPICKER = {
    "SOURCE": "cdn",        # "cdn" (default) | "vendored"
    "VERSION": "2.5.21",    # pinned upstream version, used by both modes
    "CDN_BASE": "https://cdn.jsdelivr.net/npm/jquery-datetimepicker@{version}/build/",
    "VENDOR_DIR": None,     # required for vendored mode; must be on STATICFILES_DIRS
    "EMIT": True,           # False: keep registered but let a bundler deliver the files
    "LANG": None,           # None: the active request language
    "DEFAULTS": {},         # merged into every widget's picker config
}
CRUD_VIEWS_DATETIMEPICKER = {"SOURCE": "vendored", "VENDOR_DIR": BASE_DIR / "vendored_static"}
STATICFILES_DIRS = [BASE_DIR / "vendored_static"]

python manage.py cv_vendor_datetimepicker

A system check warns on startup when the vendored files are missing (crud_views.W330) or don't match the pinned VERSION (crud_views.W331).

django-pipeline

CDN mode cannot be bundled. For pipeline, use vendored mode and build your bundle from the same path list the app uses internally — then switch off tag emission:

from crud_views_widget_datetimepicker.assets import source_files

_dtp = source_files("2.5.21", minified=False)   # pipeline minifies itself
PIPELINE = {
    "JAVASCRIPT": {"main": {"source_filenames": [..., *_dtp["js"]], ...}},
    "STYLESHEETS": {"main": {"source_filenames": [..., *_dtp["css"]], ...}},
}
CRUD_VIEWS_DATETIMEPICKER = {"SOURCE": "vendored", "VENDOR_DIR": ..., "EMIT": False}

Keep the order jQuery → plugin → init.js in your bundle (source_files() already returns plugin before init.js).

crud_views_widget_typeahead

A server-backed type-ahead single select for large, permission-sensitive model choice sets (thousands of users), for forms and django-filter filters. Vanilla JS, no jQuery, CSP-safe, ARIA combobox.

Install

INSTALLED_APPS = [
    # ...
    "crud_views.apps.CrudViewsConfig",
    "crud_views_widget_typeahead.apps.CrudViewsWidgetTypeaheadConfig",
]

# urls.py — any prefix
urlpatterns += [path("_typeahead/", include("crud_views_widget_typeahead.urls"))]

Assets reach every page through {% cv_js %} / {% cv_css %}.

Define a lookup

Put lookups in your app's typeahead.py; it is imported automatically at startup.

from crud_views_widget_typeahead.lookups import register_user_lookup

@register_user_lookup("projects.members")
def members(request, project):
    if not request.user.has_perm("projects.view_project"):
        raise PermissionDenied
    return get_object_or_404(Project, pk=project).members.all()

For other models, subclass TypeaheadLookup (search_fields, ordering, max_results=10, min_chars=2, login_required=True, get_queryset(request, **params), label(obj)) and decorate it with @register_lookup("app.name"). Values are always the pk (to_field_name is not supported). Every word of the search term must match one of search_fields (icontains). register_user_lookup(name, base=...) takes your own UserLookup subclass, e.g. to change the label.

Bind a form field / filter

class TaskForm(forms.ModelForm):
    assignee = TypeaheadField(lookup="projects.members")

    def __init__(self, *args, request, project, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["assignee"].bind(request, project=str(project.pk))


class TaskFilter(django_filters.FilterSet):
    assignee = TypeaheadFilter(lookup="projects.members")

    do_not_call_in_templates = True  # see below

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.filters["assignee"].bind(self.request, project=str(self.project.pk))

bind() sets the field's queryset (save-time validation) and signs {lookup, params} into the widget, so the endpoint serves exactly that set, re-checking permission on every request. Params must be JSON values (pass UUIDs as str). An unbound field raises ImproperlyConfigured. With crud_views' CrispyModelForm, use self.cv_view.request (see the example app).

crud_views list views: the list template tests {% if view.filterset_class %}, and Django templates call callables — so the FilterSet class is instantiated there without a request. Set do_not_call_in_templates = True on a FilterSet that binds in __init__.

Settings

Key in CRUD_VIEWS_TYPEAHEAD Default
EMIT True False suppresses our cv_js/cv_css output (skins, django-pipeline)
TEMPLATE "crud_views_widget_typeahead/typeahead.html" widget template
DEBOUNCE_MS 250 input debounce
SELECT_EMIT True False suppresses the select popup's cv_js/cv_css output (skins, django-pipeline)
SELECT_TEMPLATE "crud_views_widget_typeahead/typeahead_select.html" select popup template

django-pipeline: from crud_views_widget_typeahead.assets import source_files, select_source_files (Django-free). source_files() is the text typeahead only; add select_source_files() if you use the popup. DEBOUNCE_MS applies to both widgets.

Endpoint

GET <mount>/?t=<token>&q=<term>[&page=<n>] → {"results": [{"value", "label"}], "count": n} (count before truncation). page is 1-based with max_results rows per page (missing or invalid → 1); results are ordered by the lookup's ordering with the pk as final tiebreaker, so pages never repeat or skip rows. Errors are JSON: 400 bad_token, 403 login_required / forbidden, 404 not_found; 405 for non-GET. Terms shorter than min_chars return an empty result without a query.

Select popup

TypeaheadSelect is a second widget for the same fields and filters: a trigger that looks like a Bootstrap <select>, opening a popup with a search field and a list that loads the next page when scrolled to the end. Choose it per field; both widgets can appear on one page.

from crud_views_widget_typeahead.widgets import TypeaheadSelect

assignee = TypeaheadField(lookup="projects.members", widget=TypeaheadSelect())
assignee = TypeaheadFilter(lookup="projects.members", widget=TypeaheadSelect(submit_on_clear=True))

bind() works exactly as above. A lookup with min_chars = 0 lists its first page as soon as the popup opens; otherwise the popup asks for min_chars characters first. Keyboard: Enter, Space or ↑/↓ (or typing) opens; ↑/↓ move, Enter selects, Escape closes only the popup (a surrounding crud_views modal stays open); Enter never submits while it is open. The popup is rendered inside the widget, so it works in crud_views modals; it can be clipped by an ancestor with overflow: hidden and inside modal-dialog-scrollable modals (it is scrolled within the modal body).

Its skin contract mirrors the one below: SELECT_TEMPLATE (or the widget's template_name) gets widget.typeahead with every key listed under Skins plus popup_id, label_id, search_id, page_size, and i18n keys placeholder, search, loading, type_more; register your own JS/CSS and set SELECT_EMIT = False; keep the hidden-input contract and the behaviours above. Dynamically added content needs window.cvTypeaheadSelect.init(container).

Skins

A design-system package replaces the frontend and keeps the server core:

  1. Set CRUD_VIEWS_TYPEAHEAD["TEMPLATE"] (or a widget's template_name). The template gets widget.attrs (visible input: id, ARIA, merged aria-describedby) and widget.typeahead: url, label, value, hidden_name, hidden_id, listbox_id, hint_id, min_chars, debounce, submit_on_clear, i18n (hint, more, none, count, error, clear, listbox; {n} is the number placeholder).
  2. Register your own JS/CSS with crud_views.lib.assets.register_assets and set EMIT = False.
  3. Keep the hidden input contract (it has the name, carries the pk, autocomplete="off", empty when nothing is selected) and these behaviours: visible text = label of the stored pk except while typing (restore on blur and submit; clear only via a clear button or fully emptied text); arrow keys preview text only; Enter never submits while the list is open, nor while typed text is not yet a selection; escape/highlight labels yourself; show "N more" from count; ARIA combobox/listbox with a live result count; re-init on cv:modal:loaded.

Dynamically added content (e.g. new formset rows) needs window.cvTypeahead.init(container).

Example app

examples/bootstrap5/ is a local Django project with one page per use case (datetimepicker: forms, modal, list filters, per-field config; typeahead: teams and tasks with 3,000 users). It runs against the code in src/.

cd examples/bootstrap5
task run        # migrate, seed, runserver on :8000 — log in as admin/admin
task test       # the example's own tests

Without Task: uv run manage.py migrate && uv run manage.py seed && uv run manage.py runserver.

Metadata

Release files for django-crud-views-extensions 0.3.0

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

Source distribution (sdist)

Source distribution for django-crud-views-extensions 0.3.0
File Size Uploaded
django_crud_views_extensions-0.3.0.tar.gz 188.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-crud-views-extensions 0.3.0
File Interpreter ABI Platform
django_crud_views_extensions-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 224.6 kB

Release files / django_crud_views_extensions-0.3.0.tar.gz

Download URL django_crud_views_extensions-0.3.0.tar.gz
Size 188.5 kB
Tags Source
SHA-256 checksum
How to use checksums
83a49e62f2b7500f1d6081bea4edb057698c3d69c2f0fbb500825f9327b7aa3b
BLAKE2b-256 checksum
How to use checksums
b928837af8b03420375e65369f64f3b2716745dff63f249085345af9dceb215d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release files / django_crud_views_extensions-0.3.0-py3-none-any.whl

Download URL django_crud_views_extensions-0.3.0-py3-none-any.whl
Size 36.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
020f93f17f0c61b94025a0e8a7c557cb17138f2b73d6849c4d309f52dd101939
BLAKE2b-256 checksum
How to use checksums
dd181a9e8fc22facb6cf02f28f5313984d21079ffc565e882986ba1736f323ad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.2

2 release files

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

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