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

pytest -m "not splinter" -n auto

Default addopts in pyproject.toml enable parallel runs

Browser (Splinter)

pytest -m splinter -n 0 --splinter-headless

Must use -n 0 (serial); needs Firefox + geckodriver

Full Python suite

tox

Runs both commands above

JavaScript (Vitest)

npm test

happy-dom; see vitest.config.js

Tutorial integrity

pytest -m tutorial -n 0

Validates literalinclude paths in docs

Doc screenshots

see Updating screenshots

Always -n 0

Pytest configuration

Configuration lives in pyproject.toml ([tool.pytest.ini_options]). Tests use the dmvc_example Django project (DJANGO_SETTINGS_MODULE=dmvc_example.settings).

Pytest markers

Marker

Purpose

splinter

Browser tests; always run with -n 0

docs_screenshot

Captures PNGs into docs/_static/screenshots/

tutorial

Validates tutorial example apps referenced from docs

django_db

Needs database access

bulma / bootstrap

Frontend-specific (reserved)

Shared fixtures and helpers

tests/conftest.py provides:

  • _autodiscover_routes (autouse) — calls djmvc.site.build() before each test

  • admin_user — pytest-django superuser (admin / password)

  • browser_login — Splinter login helper

  • many_users, stage0_bulk_items — seeded data for browser tests

tests/alight_helpers.py has DAL/autocomplete helpers for Splinter tests (wait_alight_ready, type_and_select, open_autocomplete, etc.).

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 files

  • docs/reference/ — API reference

  • docs/_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

docs/_static/screenshots/

Yes

PNGs embedded in Sphinx via .. figure:: /_static/screenshots/<name>.png

tests/screenshots/dal/, tests/screenshots/topbar/

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) — writes docs/_static/screenshots/<name>.png

  • prepare_browser(browser) — sets viewport to 1280×900

  • DOC_SCREENSHOTS — registry of expected PNG names

  • assert_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

  1. Add .. figure:: /_static/screenshots/<name>.png in the appropriate .rst file.

  2. Capture it in tests/test_docs_screenshots.py (or an existing Splinter test) using from doc_screenshots import capture.

  3. Add <name> to DOC_SCREENSHOTS in tests/doc_screenshots.py.

  4. 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

src/djmvc_bulma/static/djmvc_bulma/js/

Core UI components

src/djmvc_dal_topbar/static/djmvc_dal_topbar/js/

Extension components (e.g. site search)

src/djmvc_bulma/static/djmvc_bulma/css/style.css

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 (HTMLElement)

Interactive UI tied to server-rendered children inside the tag

<list-action-bar>, <filter-sidebar-toggle>, <hamburger-menu>

Unpoly compiler module

Attach behavior when Unpoly compiles new DOM fragments

form-focus.js, toast.js, nav-config.js

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 with queueMicrotask(() => this.init()).

  • disconnectedCallback() unbinds listeners where needed.

  • Listen for up:fragment:inserted when the feature must survive [up-list] / [up-table] partial updates (see list-action-bar.js).

  • Config via HTML attributes (table, scope, target) and data-* 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 barsrc/djmvc_bulma/templates/djmvc/list.html wraps bulk actions in <list-action-bar> and passes i18n labels via data-count-label-* attributes.

Python bridgesrc/djmvc/views/list_action.py:

  • ListActionMixin.unpoly_attributes(context='list_action_bar') sets data-list-action="urlupdate" and up-on-accepted calling djmvcClearListActionSelections().

  • form_attributes sets up-on-finished='djmvcClearListActionSelections()'.

  • Views with tags = ['list_action'] are discovered for the action bar.

Templatetagssrc/djmvc/templatetags/djmvc.py provides unpoly_attributes and html_attributes for wiring Unpoly from Python.

State persistencesessionStorage with prefixed keys (e.g. djmvc:list-action:{scope}?{query} for selected row PKs across pagination).

Testing JavaScript changes

  1. Vitest (fast) — add <name>.test.js beside the source file; register it in vitest.config.js include; mock up where needed.

  2. pytest HTML (no browser) — assert rendered HTML contains custom element tags (e.g. tests/test_stage4.py, tests/test_filter.py).

  3. pytest Splinter (integration) — click elements, run browser.execute_script(...) against component APIs (e.g. tests/test_list_action.py).

Adding a new component

  1. Pick a pattern (custom element vs up.compiler).

  2. Add *.js under the appropriate static/.../js/ directory.

  3. Include the script in base.html or a feature partial.

  4. Use the custom element tag and attributes in a Django template.

  5. Add Vitest coverage and pytest coverage for user-facing behavior.

  6. If the UI appears in docs, add a screenshot capture (see Updating screenshots).

Code style

  • Python: black and ruff, 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 0 and make -C docs html (if docs or

    tutorial changed)

  • [ ] Regenerate and commit doc screenshots (if Bulma UI shown in docs changed)