Rich Author Multi-Select

Example Overview

The Rich Author Multi-Select example pairs with the Rich Article Select demo and pushes the idea further: three multi-select widgets on one page, all driven by the same autocomplete endpoint, each rendering the data with a different visual treatment and a different TomSelect plugin combination.

This is the place to look when you want to compare side-by-side what attrs["render"] lets you do without changing the backend. The same author payload powers a minimal “slim” card, a data-viz-forward “stats” card with an inline SVG sparkline and a global peer rank, and an information-dense “full” card with a status-mix bar, top categories, and an activity indicator.

Objective:

  • Demonstrate multi-select with TomSelectModelMultipleChoiceField, including PluginRemoveButton, PluginClearButton, and PluginCheckboxOptions.

  • Showcase three different option templates rendered from one shared payload, so the lesson “same data, different rendering” is explicit.

  • Illustrate how to compute and surface non-trivial per-row data (deterministic gradient avatars, status buckets, monthly sparklines, global peer rank) without pulling in a charting library.

  • Show how to handle POST of a multi-widget form and present per-widget summary cards that aggregate the user’s selections.

Use Case:

  • Editorial dashboards where editors pick co-authors, reviewers, or contributors for a piece.

  • Any “people picker” interface where richer context (recent activity, area of expertise, output volume) helps the user choose confidently.

  • Internal admin or CRM screens where comparing several rendering styles for the same dataset is useful when prototyping.

Visual Examples

Screenshot: Rich Author Multi-Select - slim card, dropdown open Screenshot: Rich Author Multi-Select - stats-forward, dropdown open Screenshot: Rich Author Multi-Select - full kit, dropdown open Screenshot: Rich Author Multi-Select - submitted summary cards


Key Code Segments

Forms

The form has three multi-select fields, all required=False so partial submissions work. All three point at the same autocomplete-rich-author endpoint and receive the same rich payload - only the attrs["render"] templates and plugin combinations differ.

A small _rich_author_base_config_kwargs() helper keeps the shared TomSelectConfig settings DRY (URL, value/label fields, preload, minimum query length, css framework, placeholder).

Item templates use only data.name (with a defensive client-side fallback to compute initials from the name). The TomSelectModelMultipleWidget re-hydrates a bound form’s selected items with only {value, label} - any extra keys would render as undefined for retained selections, so visual richness lives entirely in the option templates.


Templates

The page extends base_with_bootstrap5.html. The extra_header block carries Bootstrap Icons, custom CSS for the gradient avatars (six palette gradients keyed off a stable hash of the author’s name), the status-mix bar, sparkline bars, and the per-widget showcase cards. The content block renders the three widgets in showcase cards from slim to stats to full, with each card conditionally including a summary partial after a POST.

A small partial (_author_selection_summary.html) accepts summary and variant ("full", "slim", or "stats") and emphasizes different stats per widget.


View

The view handles GET and POST. On POST, it always re-renders an unbound form (so widgets start clean - because item templates only carry name, retained chips would otherwise display partial data), and it adds a per-widget summary card to the context for each field that had selections. A small private helper aggregates total articles, distinct magazines, shared categories, and global peer ranks across the selections.


Autocomplete View

RichAuthorAutocompleteView annotates the queryset with article and magazine counts, prefetches the author’s articles (with select_related("magazine") and prefetch_related("categories")) for N+1-free Python aggregation, and overrides search() to require every whitespace-separated term to match name OR bio (the same AND-of-OR pattern used by RichArticleAutocompleteView).

prepare_results() builds the dict shape from scratch and adds derived fields used by the three render templates: stable avatar_palette_index (via zlib.adler32), initials, bio_snippet, activity_level (recent / medium / old / never), top_categories (top 2 by occurrence), status_mix (three integer-percent buckets that sum to exactly 100), monthly_sparkline (12 integer counts) and sparkline_bars (server-normalized to 0…100 for direct use in the SVG), expertise, years_active, and a global peer_rank computed from a per-request rank map (not a window function inside the filtered subset).


Design and Implementation Notes

Why three widgets, one endpoint?

The whole point of the demo is the lesson: with attrs["render"], the same server payload can drive radically different visual treatments. Wiring three endpoints would teach the wrong thing. The trade-off is a slightly larger payload for the slim widget (it ignores fields like sparkline_bars and status_mix); for a real production app you would typically have widgets pick their own subset.

Plugins per widget

Widget

Plugins

UX hint

Slim

PluginRemoveButton

Minimal chrome, fastest perception

Stats

PluginRemoveButton + PluginCheckboxOptions

Checkbox rows reinforce the “build a panel” feel

Full

PluginRemoveButton + PluginClearButton

Bulk clear is helpful when the dropdown is information-dense

PluginDropdownHeader is intentionally not used here - it adds a tabular column header row that does not line up with the rich card layout, and would feel like visual noise.

Why item templates only reference data.name

Tom Select’s TomSelectModelMultipleWidget._get_selected_options rebuilds bound-form selected items as {value, label} only (plus optional url fields). Item templates that referenced activity_level, peer_rank, top_categories, etc., would render undefined for retained selections after a POST. To keep the post-submit chip behavior clean, item templates use only data.name (with a defensive client-side fallback to compute initials). All visual richness lives in the option template, which is rendered from full live AJAX payloads.

After form submission the view also renders an unbound form, so even the limited item template never runs on partial data.

Stable avatar gradients across restarts

Avatar colors are derived from zlib.adler32(author.name.encode("utf-8")) % 6. Python’s built-in hash() is randomized between processes, so it would produce different colors on every server restart. adler32 is stable and deterministic, which keeps the same author looking the same color across page reloads.

Status mix that always sums to 100%

The status_mix array contains three buckets - published (PUBLISHED / ACTIVE), draft (DRAFT / PENDING / ON_REVIEW / NEEDS_REVIEW / IN_PROGRESS / WIP), and other (everything else, including archived, canceled, etc.). Integer percentages are computed from raw counts, then the rounding remainder is added to the largest bucket so the bar always fills to exactly 100%. Exact-match allowlists are used rather than substring matching so “unpublished” is not miscounted as “published”.

Global peer rank, not subset rank

Peer rank is computed once per autocomplete request from a fresh queryset that is not filtered by the search terms. A Django Window function inside hook_queryset() would have ranked only within the filtered subset, which would have been misleading. The extra query is cheap and bounded by the total author count.

Server-side sparkline normalization

sparkline_bars is computed as round(100 * count / max(monthly_sparkline)) for each of the 12 months. The SVG template multiplies by 22/100 to map into the 24-pixel-tall viewBox, with a Math.max(1, ...) floor so non-zero months always show a visible bar. Doing the normalization server-side means the JS template is purely declarative.

Verifying the demo end-to-end

  • Spin up the dev server: uv run python manage.py runserver

  • Visit http://localhost:8000/rich-author-multi-select/

  • Open each dropdown; confirm gradient avatars stay stable across page reloads, status mix bars sum visually to 100%, and the sparkline renders 12 bars.

  • Select a few authors in each widget and submit. The three summary cards should appear underneath their respective widgets; the widgets themselves reset to empty.