Read Query Parameters¶
Problem¶
A listing page needs the search term, page number, and selected filters from the query string.
You want typed values without writing request.GET.get(...) plumbing in every callable.
Solution¶
Annotate a @context parameter with the DQuery[T] marker.
The framework reads request.GET, coerces the value to the annotated type, and injects it.
DQuery supports str, int, bool, float, UUID, Decimal, date, datetime, and list[T] for multi-value parameters.
Walkthrough¶
Read a Single Parameter¶
Declare the parameter with a type and a default. The default is used when the key is absent from the query string.
from catalog.models import Product
from next.pages import context
from next.urls import DQuery
DEFAULT_FEATURED = 3
MAX_FEATURED = 12
@context("featured")
def featured(show: DQuery[int] = DEFAULT_FEATURED) -> list[Product]:
count = max(1, min(MAX_FEATURED, show if isinstance(show, int) else DEFAULT_FEATURED))
return list(
Product.objects.filter(in_stock=True)
.select_related("category")
.order_by("-created_at")[:count],
)
A request to /?show=8 injects show=8 as an int.
A request to / injects the default 3.
Type Coercion¶
The annotation drives coercion of the raw query string.
Note
The marker only coerces the type.
Clamp or otherwise bound the value yourself, as the featured example does with max and min.
DQuery[int].Parsed with
int(). A value that does not parse, such as?show=abc, falls back to the raw string rather than raising. Validate when a bad value must be rejected.DQuery[float].Parsed with
float(), with the same string fallback on a parse failure.DQuery[bool].Truewhen the value is1,true, oryes, case-insensitive. Every other value, including0andfalse, isFalse.DQuery[UUID].Parsed with
UUID(...). A value that does not parse falls back to the raw string.DQuery[Decimal].Parsed with
Decimal(...). A value that does not parse falls back to the raw string.DQuery[date]andDQuery[datetime].Parsed via
date.fromisoformatanddatetime.fromisoformat. A value that does not parse falls back to the raw string.DQuery[str].The raw value, unchanged.
DQuery[list[T]].Three wire formats are accepted, but only one is consulted per request.
request.GET.getlist(name)runs first. When it returns more than one value, the plain repeated form?brand=Acme&brand=Globexis used as-is. When the plain value is empty, the resolver falls back to the bracket form?brand[]=Acme&brand[]=Globex. When the plain value is a single non-empty string that contains a comma, the resolver falls back to the comma-delimited form?brand=Acme,Globex. Each element is then coerced using the same rules as the scalar form forT.
A scalar parameter that is absent from the query string receives the declared default, or None when no default is given.
A DQuery[list[T]] parameter that is absent in all three wire formats returns the declared default, or an empty list when no default is given.
Read Several Typed Parameters¶
A single callable can mix scalar and list parameters. Each annotation drives its own coercion.
from next.pages import context
from next.urls import DQuery
@context("results")
def results(
q: DQuery[str] = "",
page: DQuery[int] = 1,
in_stock: DQuery[bool] = False,
brand: DQuery[list[str]] = (),
) -> dict:
return {"q": q, "page": page, "in_stock": in_stock, "brands": list(brand)}
List elements follow the same three wire formats described above under Type Coercion.
Build a Typed Snapshot With a Provider¶
When several callables need the same filter set, parse it once into a frozen dataclass.
get_multi_values reads a multi-value parameter through the same three wire formats as DQuery[list[T]].
from dataclasses import dataclass
from next.urls import get_multi_values
@dataclass(frozen=True, slots=True)
class Filters:
q: str = ""
brands: tuple[str, ...] = ()
in_stock: bool = False
sort: str = "newest"
def parse_filters(request):
g = request.GET
return Filters(
q=g.get("q", "").strip(),
brands=tuple(get_multi_values(request, "brand")),
in_stock=g.get("in_stock", "").lower() in {"1", "true", "yes"},
sort=g.get("sort") or "newest",
)
Render the Form¶
Search is idempotent, so the filter form uses method="get" and posts back to the same page.
A bookmarked URL reproduces the same listing.
Reserve @action for POST side effects such as creating or deleting rows.
<form method="get" action="{{ submit_url }}" data-filter-form>
<input name="q" type="search" value="{{ current_filters.q }}"/>
{% for brand in all_brands %}
<label>
<input type="checkbox" name="brand" value="{{ brand }}"
{% if brand in current_filters.brands %}checked{% endif %}/>
{{ brand }}
</label>
{% endfor %}
{% component "button" type="submit" text="Apply filters" variant="default" %}
</form>
Verification¶
Open the listing page with a faceted query string and confirm the response reflects every parameter.
uv run python manage.py runserver
Visiting /catalog/?q=iphone&brand=Acme&brand=Globex&in_stock=1&page=2 filters by search term, two brands, and stock, on the second page.
The bracket form ?brand[]=Acme&brand[]=Globex and the comma form ?brand=Acme,Globex produce the same listing.
See Also¶
See also
Dependency Injection for the built-in providers.
Reverse URLs for building query strings from code.
URL Reversing for DUrl on the path versus DQuery in the query string.
File Router for bracket segments that become URL kwargs.