Use complete words for variable names; document the convention
Rename abbreviated identifiers in the PR's code to full words: tpl→template, e→event, el→element, removeBtn→removeButton, and single-letter loop variables (o→option, g/d/p→game/device/platform, v→value/modifier_value). Add a 'name variables with complete words' convention to CLAUDE.md. https://claude.ai/code/session_01XzhXvMvw42CQGc9kmin3GS
This commit is contained in:
@@ -158,6 +158,7 @@ Pytest settings are in `pyproject.toml` under `[tool.pytest.ini_options]` (`DJAN
|
||||
## Conventions for AI assistants
|
||||
|
||||
- **Never write to `GeneratedField`s** (`duration_calculated`, `duration_total`, `price_per_game`, `days_to_finish`). They are computed by the database.
|
||||
- **Name variables with complete words** — readable, unabbreviated identifiers in both Python and JavaScript (e.g. `template` not `tpl`, `event` not `e`, `element` not `el`, `removeButton` not `removeBtn`, `option`/`value` not single letters in loops). This applies to new code and to code you touch.
|
||||
- **Use `render_page()` not `render()`** for all full-page HTTP responses. Import from `common.layout`.
|
||||
- **Build UI with Python components** from `common.components`, not raw HTML strings or Django templates. `SafeText` children pass through unescaped; plain strings are auto-escaped.
|
||||
- **Filter views** accept `?filter=<JSON>` (structured) and fall back to `?search_string=` (free-text). New filter criteria go in `games/filters.py`; new criterion types go in `common/criteria.py`.
|
||||
|
||||
@@ -106,8 +106,8 @@ def _resolve_game_options(ids):
|
||||
from games.models import Game
|
||||
|
||||
return [
|
||||
{"value": g.id, "label": g.search_label}
|
||||
for g in Game.objects.filter(pk__in=ids)
|
||||
{"value": game.id, "label": game.search_label}
|
||||
for game in Game.objects.filter(pk__in=ids)
|
||||
]
|
||||
|
||||
|
||||
@@ -116,7 +116,10 @@ def _resolve_device_options(ids):
|
||||
return []
|
||||
from games.models import Device
|
||||
|
||||
return [{"value": d.id, "label": d.name} for d in Device.objects.filter(pk__in=ids)]
|
||||
return [
|
||||
{"value": device.id, "label": device.name}
|
||||
for device in Device.objects.filter(pk__in=ids)
|
||||
]
|
||||
|
||||
|
||||
def _resolve_platform_options(ids):
|
||||
@@ -125,7 +128,8 @@ def _resolve_platform_options(ids):
|
||||
from games.models import Platform
|
||||
|
||||
return [
|
||||
{"value": p.id, "label": p.name} for p in Platform.objects.filter(pk__in=ids)
|
||||
{"value": platform.id, "label": platform.name}
|
||||
for platform in Platform.objects.filter(pk__in=ids)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -63,8 +63,9 @@ _ROW_HEIGHT_REM = 2.25
|
||||
|
||||
# ── FilterSelect styling ───────────────────────────────────────────────────
|
||||
# Inline class strings (ported verbatim from the retired SelectableFilter CSS)
|
||||
# so the filter combobox is fully self-styled — nothing in input.css. The
|
||||
# JS-built filter rows/pills in search_select.js mirror these byte-for-byte.
|
||||
# so the filter combobox is fully self-styled — nothing in input.css. JS-added
|
||||
# rows/pills are cloned from server-rendered <template>s, so these strings live
|
||||
# only here — never duplicated in search_select.js.
|
||||
_FILTER_INCLUDE_PILL_CLASS = (
|
||||
"inline-flex items-center gap-1 px-2 py-0.5 text-sm rounded "
|
||||
"bg-brand/15 text-heading"
|
||||
@@ -220,8 +221,8 @@ def SearchSelect(
|
||||
autofocus: bool = False,
|
||||
) -> SafeText:
|
||||
"""Render the search-select widget. See module docstring for the contract."""
|
||||
selected = [_normalize_option(o) for o in (selected or [])]
|
||||
options = [_normalize_option(o) for o in (options or [])]
|
||||
selected = [_normalize_option(option) for option in (selected or [])]
|
||||
options = [_normalize_option(option) for option in (options or [])]
|
||||
|
||||
# ── Pills + their hidden inputs (the submitted channel) ──
|
||||
# Multi-select renders a removable Pill per value; single-select renders no
|
||||
@@ -267,7 +268,7 @@ def SearchSelect(
|
||||
search_attrs.append(("value", search_value))
|
||||
|
||||
# ── Options panel (pre-rendered only when there is no search_url) ──
|
||||
option_rows = [_option_row(o) for o in options] if not search_url else []
|
||||
option_rows = [_option_row(option) for option in options] if not search_url else []
|
||||
|
||||
# ── Templates the JS clones: a row when results are fetched, a pill when
|
||||
# multi-select adds chosen items. ──
|
||||
@@ -430,9 +431,9 @@ def FilterSelect(
|
||||
labels even when the value rows come from ``search_url``. ``options``
|
||||
pre-renders the value rows for the complete-set (no ``search_url``) case.
|
||||
"""
|
||||
options = [_normalize_option(o) for o in (options or [])]
|
||||
included = [_normalize_option(o) for o in (included or [])]
|
||||
excluded = [_normalize_option(o) for o in (excluded or [])]
|
||||
options = [_normalize_option(option) for option in (options or [])]
|
||||
included = [_normalize_option(option) for option in (included or [])]
|
||||
excluded = [_normalize_option(option) for option in (excluded or [])]
|
||||
modifier_options = modifier_options or []
|
||||
|
||||
active_modifier_label = ""
|
||||
@@ -468,9 +469,11 @@ def FilterSelect(
|
||||
|
||||
# ── Options: pinned modifier rows, then value rows (pre-rendered only when
|
||||
# there is no search_url; otherwise the JS fetches them) ──
|
||||
modifier_rows = [_filter_modifier_row(v, label) for v, label in modifier_options]
|
||||
modifier_rows = [
|
||||
_filter_modifier_row(value, label) for value, label in modifier_options
|
||||
]
|
||||
value_rows = (
|
||||
[_filter_option_row(o["value"], o["label"]) for o in options]
|
||||
[_filter_option_row(option["value"], option["label"]) for option in options]
|
||||
if not search_url
|
||||
else []
|
||||
)
|
||||
@@ -526,4 +529,4 @@ def searchselect_selected(
|
||||
"""
|
||||
if not values:
|
||||
return []
|
||||
return [_normalize_option(o) for o in resolver(values)]
|
||||
return [_normalize_option(option) for option in resolver(values)]
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* state into data-included / data-excluded / data-modifier for the filter bar.
|
||||
*
|
||||
* initAll() runs on DOMContentLoaded + htmx:afterSwap, each widget guarded with
|
||||
* el._ssInit.
|
||||
* element._ssInit.
|
||||
*
|
||||
* Dynamically-added rows and pills are cloned from hidden <template> elements
|
||||
* the server renders with the same Python components (Pill / SearchSelect /
|
||||
@@ -27,10 +27,10 @@
|
||||
var DEBOUNCE_MS = 500;
|
||||
|
||||
function initAll() {
|
||||
document.querySelectorAll("[data-search-select]").forEach(function (el) {
|
||||
if (el._ssInit) return;
|
||||
el._ssInit = true;
|
||||
initWidget(el);
|
||||
document.querySelectorAll("[data-search-select]").forEach(function (element) {
|
||||
if (element._ssInit) return;
|
||||
element._ssInit = true;
|
||||
initWidget(element);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,8 +79,10 @@
|
||||
// ── Clone a server-rendered <template> prototype by name. The server emits
|
||||
// the mode-appropriate prototypes, so the JS never names a class. ──
|
||||
function cloneTemplate(name) {
|
||||
var tpl = container.querySelector('template[data-ss-tpl="' + name + '"]');
|
||||
return tpl ? tpl.content.firstElementChild.cloneNode(true) : null;
|
||||
var template = container.querySelector('template[data-ss-tpl="' + name + '"]');
|
||||
return template
|
||||
? template.content.firstElementChild.cloneNode(true)
|
||||
: null;
|
||||
}
|
||||
|
||||
function setLabel(node, label) {
|
||||
@@ -214,24 +216,24 @@
|
||||
}
|
||||
|
||||
// Clicking an option must not blur the input before the click selects.
|
||||
options.addEventListener("mousedown", function (e) {
|
||||
e.preventDefault();
|
||||
options.addEventListener("mousedown", function (event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// ── Option click → select (form mode) or include/exclude (filter mode) ──
|
||||
options.addEventListener("click", function (e) {
|
||||
options.addEventListener("click", function (event) {
|
||||
if (isFilter) {
|
||||
handleFilterOptionClick(e);
|
||||
return;
|
||||
}
|
||||
var row = e.target.closest("[data-ss-option]");
|
||||
var row = event.target.closest("[data-ss-option]");
|
||||
if (!row) return;
|
||||
selectOption(optionFromRow(row));
|
||||
});
|
||||
|
||||
function handleFilterOptionClick(e) {
|
||||
// Pinned modifier pseudo-option → set the (mutually exclusive) modifier.
|
||||
var modifierRow = e.target.closest("[data-ss-modifier-option]");
|
||||
var modifierRow = event.target.closest("[data-ss-modifier-option]");
|
||||
if (modifierRow) {
|
||||
setModifier(
|
||||
modifierRow.getAttribute("data-ss-modifier-option"),
|
||||
@@ -240,7 +242,7 @@
|
||||
return;
|
||||
}
|
||||
// Include / exclude button on a value row.
|
||||
var button = e.target.closest("[data-ss-action]");
|
||||
var button = event.target.closest("[data-ss-action]");
|
||||
if (!button) return;
|
||||
var row = button.closest("[data-ss-option]");
|
||||
if (!row) return;
|
||||
@@ -343,10 +345,10 @@
|
||||
}
|
||||
|
||||
// ── Pill × → remove ──
|
||||
pills.addEventListener("click", function (e) {
|
||||
var removeBtn = e.target.closest("[data-pill-remove]");
|
||||
if (!removeBtn) return;
|
||||
var pill = removeBtn.closest("[data-pill]");
|
||||
pills.addEventListener("click", function (event) {
|
||||
var removeButton = event.target.closest("[data-pill-remove]");
|
||||
if (!removeButton) return;
|
||||
var pill = removeButton.closest("[data-pill]");
|
||||
if (!pill) return;
|
||||
if (isFilter) {
|
||||
// Filter pills have no hidden input; a modifier pill also clears the
|
||||
@@ -404,8 +406,8 @@
|
||||
}
|
||||
|
||||
// ── Close panel on outside click ──
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!container.contains(e.target)) hidePanel();
|
||||
document.addEventListener("click", function (event) {
|
||||
if (!container.contains(event.target)) hidePanel();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user