Contributing¶
djmvc is a Django MVC layer with a Bulma frontend. Most contributors work in Python; the JavaScript is thin glue over server-rendered HTML, not a separate frontend application.
The example project used by tests lives in src/djmvc_example/.
Development setup¶
git clone https://github.com/jpic/djmvc.git
cd djmvc
pip install --pre -e ".[dev,docs]"
npm ci # only needed for JavaScript unit tests
Browser tests require Firefox and geckodriver on your PATH (CI installs
both via browser-actions/setup-firefox and
browser-actions/setup-geckodriver).
Documentation builds need the [docs] extra from pyproject.toml
(or docs/requirements.txt).
Running tests¶
Run the same checks as CI before opening a pull request.
Layer |
Command |
Notes |
|---|---|---|
Fast Python |
|
Default |
Browser (Splinter) |
|
Must use |
Full Python suite |
|
Runs both commands above |
JavaScript (Vitest) |
|
happy-dom; see |
Tutorial integrity |
|
Validates |
Doc screenshots |
Always |
Pytest configuration¶
Configuration lives in pyproject.toml ([tool.pytest.ini_options]).
Tests use the dmvc_example Django project
(DJANGO_SETTINGS_MODULE=dmvc_example.settings).
Marker |
Purpose |
|---|---|
|
Browser tests; always run with |
|
Captures PNGs into |
|
Validates tutorial example apps referenced from docs |
|
Needs database access |
|
Frontend-specific (reserved) |
Subset examples¶
pytest tests/test_stage0.py -v
pytest tests/test_list_action.py -n 0 --splinter-headless
npm run test:ui # optional Vitest UI
Documentation¶
Docs are Sphinx with the Furo theme under docs/.
make -C docs html
# preview: docs/_build/html/index.html
CI (.github/workflows/docs.yml) runs pytest -m tutorial -n 0 then
make -C docs html on every push and pull request.
Layout:
docs/tutorial/— staged tutorial RST filesdocs/reference/— API referencedocs/_static/screenshots/— committed PNGs embedded in RST
When editing tutorial code shown via .. literalinclude:: in
docs/**/*.rst, keep paths valid — tests/test_tutorial_docs.py
fails if a referenced file is missing or moved.
Updating screenshots¶
There are two screenshot directories. Only one is used in published docs.
Directory |
Committed? |
Purpose |
|---|---|---|
|
Yes |
PNGs embedded in Sphinx via |
|
Yes (debug) |
Intermediate captures from DAL/topbar Splinter tests; not used in docs |
Regenerate all doc screenshots¶
After changing templates or navigation that appear in the docs:
pytest tests/test_docs_screenshots.py tests/test_djmvc_dal_topbar_splinter.py -n0 --splinter-headless
make -C docs html
Commit the updated PNGs under docs/_static/screenshots/ with your
change.
Helpers live in tests/doc_screenshots.py:
capture(browser, name)— writesdocs/_static/screenshots/<name>.pngprepare_browser(browser)— sets viewport to 1280×900DOC_SCREENSHOTS— registry of expected PNG namesassert_all_captured()— verifies every registered screenshot exists
Skip screenshot regeneration for pure Python changes, doc text-only edits, or tutorial code changes with no visual impact.
Adding a new doc screenshot¶
Add
.. figure:: /_static/screenshots/<name>.pngin the appropriate.rstfile.Capture it in
tests/test_docs_screenshots.py(or an existing Splinter test) usingfrom doc_screenshots import capture.Add
<name>toDOC_SCREENSHOTSintests/doc_screenshots.py.Run the regenerate command above and commit the PNG.
JavaScript for Python developers¶
djmvc JavaScript manages client-side state and DOM updates within server-rendered HTML. Python owns permissions, URLs, and markup structure.
Django view (list_actions, unpoly_attributes)
↓
Django template (custom element tags, data-* hooks)
↓
Static ES module (custom element or up.compiler)
↓
Unpoly partial updates (up:fragment:inserted, compilers)
↓
Vitest (unit) + pytest/Splinter (integration)
Where JavaScript lives¶
Location |
Role |
|---|---|
|
Core UI components |
|
Extension components (e.g. site search) |
|
Layout and component styles |
Scripts load from src/djmvc_bulma/templates/djmvc/base.html as native
ES modules (type="module"). There is no bundler — files ship via
setuptools package-data and Django {% static %}.
package.json is dev-only (Vitest + happy-dom).
Two patterns¶
Choose one when adding frontend behavior.
Pattern |
When to use |
Examples |
|---|---|---|
Custom element ( |
Interactive UI tied to server-rendered children inside the tag |
|
Unpoly compiler module |
Attach behavior when Unpoly compiles new DOM fragments |
|
Custom element conventions¶
No Shadow DOM — light DOM only. Django renders children inside the tag; JS uses
this.querySelector(...).Register with a guard so the module is safe to load more than once:
if (!customElements.get('list-action-bar')) { customElements.define('list-action-bar', ListActionBar); }
connectedCallback()wires listeners; defer init withqueueMicrotask(() => this.init()).disconnectedCallback()unbinds listeners where needed.Listen for
up:fragment:insertedwhen the feature must survive[up-list]/[up-table]partial updates (seelist-action-bar.js).Config via HTML attributes (
table,scope,target) anddata-*hooks (data-pk,data-role,data-list-action).Export classes and pure helpers for Vitest.
Expose
window.djmvc*functions when templates or Python need to call JS (e.g.djmvcClearListActionSelections).
Some toggles wrap light-DOM children in a <button type="button"> so Bulma
and Unpoly nav feedback do not strip is-active from <a> inside
<nav> (see filter-sidebar.js, hamburger.js).
Unpoly compiler conventions¶
Compiler modules export a register* function and auto-register when
window.up exists:
export function registerFormFocus(up) {
up.compiler('form[method="post"], form.djmvc-filter-form', (form) => {
queueMicrotask(() => focusFirstInput(form));
});
}
if (typeof window !== 'undefined' && window.up) {
registerFormFocus(window.up);
}
Python ↔ JavaScript contract¶
Templates and JS agree on tags, attributes, and data-* hooks.
List action bar — src/djmvc_bulma/templates/djmvc/list.html wraps
bulk actions in <list-action-bar> and passes i18n labels via
data-count-label-* attributes.
Python bridge — src/djmvc/views/list_action.py:
ListActionMixin.unpoly_attributes(context='list_action_bar')setsdata-list-action="urlupdate"andup-on-acceptedcallingdjmvcClearListActionSelections().form_attributessetsup-on-finished='djmvcClearListActionSelections()'.Views with
tags = ['list_action']are discovered for the action bar.
Templatetags — src/djmvc/templatetags/djmvc.py provides
unpoly_attributes and html_attributes for wiring Unpoly from Python.
State persistence — sessionStorage with prefixed keys (e.g.
djmvc:list-action:{scope}?{query} for selected row PKs across pagination).
Testing JavaScript changes¶
Vitest (fast) — add
<name>.test.jsbeside the source file; register it invitest.config.jsinclude; mockupwhere needed.pytest HTML (no browser) — assert rendered HTML contains custom element tags (e.g.
tests/test_stage4.py,tests/test_filter.py).pytest Splinter (integration) — click elements, run
browser.execute_script(...)against component APIs (e.g.tests/test_list_action.py).
Adding a new component¶
Pick a pattern (custom element vs
up.compiler).Add
*.jsunder the appropriatestatic/.../js/directory.Include the script in
base.htmlor a feature partial.Use the custom element tag and attributes in a Django template.
Add Vitest coverage and pytest coverage for user-facing behavior.
If the UI appears in docs, add a screenshot capture (see Updating screenshots).
Code style¶
Python:
blackandruff, line length 88 (pyproject.toml).JavaScript: ES modules, no bundler; match existing file style.
Pull request checklist¶
Before opening a PR:
[ ]
pytest -m "not splinter" -n auto- [ ]
pytest -m splinter -n 0 --splinter-headless(if UI, templates, or JS changed)
- [ ]
[ ]
npm test(if JavaScript changed)- [ ]
pytest -m tutorial -n 0andmake -C docs html(if docs or tutorial changed)
- [ ]
[ ] Regenerate and commit doc screenshots (if Bulma UI shown in docs changed)