This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Django website for the Makeability Lab at UW CSE (HCI / accessibility / urban computing research). Single Django app (website) inside a project named makeabilitylab, served via PostgreSQL and Docker. Python 3.13, Django 5.2 LTS, PostgreSQL 16.
Everything runs in Docker. There is no native venv path — work inside the container.
# First time: build image, then start
docker build . -t makelab_image
docker-compose -f docker-compose-local-dev.yml up
# Convenience wrapper (supports --build, --buildnc, --verbose)
./run-docker-local-dev.sh
# Stop
docker-compose downSite → http://localhost:8571 (container's 8000 mapped to host's 8571). Postgres exposed at host port 6543 (container 5432). The project root is bind-mounted to /code, so edits hot-reload — no rebuild needed unless Dockerfile or requirements.txt changes.
To get a shell inside the running container:
docker exec -it makeabilitylabwebsite-website-1 bash
# Some Docker Compose versions: makeabilitylabwebsite_website_1Inside the container, run Django commands as usual: python manage.py <cmd>.
A superuser is required to use /admin and add content; create one with python manage.py createsuperuser inside the container.
- Tests:
python manage.py test website --settings=makeabilitylab.settings_test(inside container). The tests live in thewebsite/tests/package (onetest_*.pyper concern; Django auto-discovers them) with shared DB fixtures inwebsite/tests/base.py. The suite has two styles:- Unit —
SimpleTestCase+MagicMockfor pure logic (formatters, BibTeX generation, etc.); no DB, runs in ms. - Integration —
DatabaseTestCase(subclass of Django'sTestCase, intests/base.py) for view / queryset / template regressions; each test runs in a transaction and rolls back. Has fixture helpersmake_person/make_publication/make_talk/make_news_item. - When fixing a bug reachable through a real queryset, URL, or view, add a regression test in the matching style before applying the fix (matches the tests-first workflow).
- Always use the
--settings=makeabilitylab.settings_testshim. It setsMIGRATION_MODULES = {'website': None}so the test DB is built directly from the current models, sidestepping the gitignored, per-environmentwebsite/migrations/history. This is the durable fix for #1267 — without it, a fresh test DB can fail at creation withcolumn "..." already exists(old workaround:docker exec makeabilitylabwebsite-db-1 psql -U admin -d postgres -c "DROP DATABASE IF EXISTS test_makeability;"). - CI:
.github/workflows/test.ymlruns this same command on every push tomasterand every PR (free/unlimited for this public repo). It reports a green ✓ / red ✗ — it does not block pushes or the deploy. See the testing roadmap in #1278. - Coverage (#1278 item 4): the CI
testjob wraps the suite incoverage runand publishes a table to the run's Summary; config is in.coveragerc(measures thewebsiteapp, branch coverage on). It's report-only — no--fail-undergate; the number targets backfill. Run locally inside the container:pip install -r requirements-dev.txt, thencoverage run manage.py test website --settings=makeabilitylab.settings_test && coverage report(orcoverage htmlfor a browsablehtmlcov/).
- Unit —
- Accessibility (Pa11y CI + Axe, WCAG 2.0 AA): start the site, then
docker-compose -f docker-compose-local-dev.yml --profile testing run --rm a11y. URLs to scan are configured in.pa11yci.json. Run this before submitting UI changes.
- Push to
master→ auto-deploys tomakeabilitylab-test.cs.washington.eduvia webhook. - Push a SemVer tag (e.g.
git tag 2.3.2 && git push --tags) → deploys to productionmakeabilitylab.cs.washington.edu. - Bump
ML_WEBSITE_VERSIONandML_WEBSITE_VERSION_DESCRIPTIONinmakeabilitylab/settings.pywhen cutting a release. - Build logs:
<host>/logs/buildlog.txt. Application logs:<host>/logs/debug.log. Seedocs/DEPLOYMENT.mdfor SSH paths onrecycle.cs.washington.edu.
The maintainer does not have shell or admin access to the test or production servers. UW CSE IT (Jason Howe) owns and configured both; Apache/web-server and file-permission changes go through them, and much of the deployed tree is apache:makelab-owned. The only available controls and visibility are:
- Deploys are push-only. Push to
master→ test; push a SemVer tag → prod. There is no way to rundockerormanage.pydirectly on either server. - SSH is read-mostly and limited to one jump host. The maintainer can SSH to
recycle.cs.washington.eduand read files on the shared CSE filesystem under/cse/web/research/makelab/(logs, themedia/dir,secret/config.ini). There is no SSH access to the host that runs the Docker stack and no passwordless sudo. - The database is not reachable directly. Prod Postgres runs as the
dbDocker container, bound to the Docker host's loopback only, so there is no tunnel/network path to it from a laptop or fromrecycle. (Credentials are moot anyway — see below.) - Therefore, any operation against prod/test data must run inside the container. Ship it as a management command wired into
docker-entrypoint.sh(the established one-shot pattern) and verify via the logs. For one-off offline analysis, request a DB snapshot from CSE IT rather than trying to connect remotely. - Never write personal or sensitive data to web-served paths (
media/,static/,logs/) — everything under them is publicly downloadable. (A stale publicdumped_data.jsonwas exactly this mistake.)
manage.py→ Django entry point, usesmakeabilitylab.settings.makeabilitylab/→ Django project (settings, root URLconf, WSGI). Root URLconf mountswebsite.urlsat/,admin.site.urlsat/admin/, ckeditor at/ckeditor/, and django-debug-toolbar at/__debug__/.website/→ the single Django app. All models, views, admin, URLs, templates live here.makeabilitylabwebsite/→ not a Python package; legacy folder holding deploy shell scripts (rebuildanddeploy.sh,command,command-test) used by the production deploy webhook. Don't confuse it with the Django project package above.sortedm2m_filter_horizontal_widget/→ a vendored, locally-modified fork of the upstreamsortedm2m-filter-horizontal-widgetpackage (upstream is incompatible with Django 5.2). It is listed inINSTALLED_APPS; treat it as project source code, not a third-party library.media/→ user-uploaded content (publications PDFs, images, talks). Bind-mounted, persists across container restarts.static/→ output ofcollectstatic; do not edit by hand. Source assets live underwebsite/static/.
Each domain concept gets a dedicated file across three parallel directories. When adding a new entity, create files in all three:
website/models/<thing>.py— the model. All models are re-exported fromwebsite/models/__init__.py, so import asfrom website.models import Person, Publication, ….website/admin/<thing>_admin.py— the ModelAdmin, registered via@admin.register(...)decorator. Imported inwebsite/admin/__init__.pypurely to trigger registration.website/views/<thing>.py— view functions, re-exported viawebsite/views/__init__.py(mostlyfrom .x import *).
Custom admin organization lives in website/admin/admin_site.py (MakeabilityLabAdminSite). It overrides Django's default app-based grouping with workflow-based groups: Artifacts (Publications/Talks/Posters/Videos), People & News, Projects & Media, Grants & Funding, Configuration, Administration. Section order and which models go in which group are defined in CUSTOM_GROUPS. Update this when adding a new top-level model that should appear on the admin index.
Admin users & permissions (#1125): editing access is structured as personal accounts assigned to one of two declarative groups — Editors (PhD/staff, full content) and Contributors (ugrads/interns, submit-and-review, no deletes) — plus superuser (maintainer + a break-glass backup). Grant, Award, and all account-administration models are superuser-only. The groups' permission sets are the source of truth in setup_admin_groups (run on every container start via docker-entrypoint.sh, pinned by test_setup_admin_groups); group membership is managed in /admin. When adding a new model that editors should manage, add it to EDITORS_MODELS/CONTRIBUTORS_SPEC and update the test. Full reference + onboarding/offboarding runbook: docs/ADMIN_USERS_AND_GROUPS.md.
- A
Publicationis the central artifact.Talk,Poster,Videoare related artifacts; the admin tip is to start from the Publication's edit page so shared fields (title, authors, date, venue) auto-fill on the children. Person↔ProjectviaProjectRole(with start/end dates). Theauto_close_project_rolesmanagement command (run on every container start) closes expired roles.Award(separate fromPublication.award) represents external recognitions; sectioned on the public Awards page byAwardType. Paper-level awards are NOTAward— they're onPublication.award. Keep this distinction in mind when modifying either.- Many M2M relations use
SortedManyToManyField(vendoredsortedm2mwidget) so display order is editor-controlled, not alphabetical.
website/urls.pyexposes both/projects/<name>/and/project/<name>/(singular) for the same view — both must keep working; project URLs are linked from external sources./media/publications/<filename>is served by the customserve_pdfview (not Django's static serve), which does fuzzy filename matching so stale external links to renamed PDFs still resolve. Don't replace it with a plain static route.- In
DEBUG=True,/media/...is also served by Django'sserve(). In production, the web server handles/media/directly.
- Compose files per environment: the servers run
docker-compose.yml(test and prod —makeabilitylabwebsite/rebuildanddeploy.shrunsdocker compose upwith no-f, so it always picks the defaultdocker-compose.yml; it only varies per-host env vars). Local dev runsdocker-compose-local-dev.yml(passed explicitly with-f).docker-compose-local-dev.ymlis never used on the servers. - Per-host wiring (set by
rebuildanddeploy.sh): test hostdocker-test2→DJANGO_ENV=TEST, mountssecret/config-test.ini+www-test/media; prod hostgrabthar→DJANGO_ENV=PROD, mountssecret/config.ini+www/media. makeabilitylab/settings.pyreadsconfig.ini(mounted at the project root, not committed) forSECRET_KEY,DEBUG, andALLOWED_HOSTS.- Prod/test
config.inihas only a[Django]section — no[Postgres]section. Persettings.py, a missing[Postgres]section means Django uses the fallbackDATABASESdefault (HOST='db') — i.e. the dockerizeddbservice of the active compose file. A[Postgres]section, if added, would override it. So the DB is the in-stackdbcontainer in every environment (no external Postgres); on the servers that's thedbservice indocker-compose.yml. DEBUGresolution order:DJANGO_ENV=PRODforces False →config.ini [Django] DEBUG→DJANGO_ENV=DEBUGforces True → default False.TIME_ZONE = 'America/Los_Angeles'.ML_WEBSITE_VERSIONin settings is shown in the admin header and used in release tagging.
Every container start runs, in order: collectstatic → makemigrations → migrate → makemigrations website → migrate website → delete_unused_files → thumbnail_cleanup → generate_slugs_for_old_news_items → auto_close_project_roles → remove_year_from_forum_name → fix_sortedm2m_columns → runserver 0.0.0.0:8000. The repeated makemigrations website step is intentional (fixes first-run issues). If you add a one-shot data migration command under website/management/commands/, decide whether it belongs in this startup sequence.
image_cropping + easy_thumbnails work together: cropping defines the crop box (stored as an "x1,y1,x2,y2" string by ImageRatioField), easy_thumbnails generates sized variants. THUMBNAIL_PROCESSORS is configured so crop_corners runs before the default chain, applying the stored box to any {% thumbnail … box=obj.cropping %} render. Image processing requires ImageMagick (installed in the Dockerfile) and a custom imagemagick-policy.xml is mounted into /etc/ImageMagick-6/policy.xml to enable PDF processing (see issue #974).
image_cropping is an in-repo fork, not the PyPI django-image-cropping (which was EOL Jcrop+jQuery, Django ≤4.0). Like sortedm2m_filter_horizontal_widget, the top-level image_cropping/ package is project source code and shadows/replaces the dropped dependency. Its admin widget is Cropper.js (vendored static, no build step): editors preview and crop client-side before the first save (#1299/#1269). The data layer is intentionally unchanged — ImageRatioField is still a CharField whose deconstruct() returns image_cropping.fields.ImageRatioField, so the gitignored per-environment migrations that import image_cropping.fields keep working and the DB column is untouched (a regression test pins this path). See image_cropping/README.md. To bump Cropper.js, replace the vendored static/image_cropping/cropper.min.{js,css} (stay on the v1.x API; v2 is a different API).
News items use django-ckeditor. Uploaded files via CKEditor land under media/uploads/, with filenames generated by website.utils.fileutils.get_ckeditor_image_filename.
.devcontainer/devcontainer.json opens VSCode inside the website service, installing Python, Pylance, the Django syntax extension, and djlint inside the container. The Dev Container connects as root (not apache) to avoid file-permission edits failing on WSL2. djlint is the formatter for *.html files under templates/; **/templates/**/*.html is associated with the django-html language.
- Favor simple, standard approaches over new frameworks/libraries. The frontend is Bootstrap + jQuery + vanilla JS; match that. Do not introduce React or a frontend build step unless explicitly requested.
- Accessibility is a first-class requirement: write a11y-correct markup by default and keep changes WCAG 2.0 AA compliant (the Pa11y service enforces this on UI changes).
- Document to language convention: JSDoc for JS, docstrings for Python views/ models/management commands. Add usage examples for non-obvious logic.
- HTML/Django templates: 2-space indentation; djlint is the formatter.
- Django template comments —
{# … #}is SINGLE-LINE ONLY. A{# … #}that spans multiple lines is NOT parsed as a comment; Django renders the whole thing (text and#}included) as visible page content. For any multi-line comment use{% comment %} … {% endcomment %}. This is a recurring footgun (it shipped to prod once as a comment printed on every award card, fixed in 2.14.2) — when adding or editing a{# … #}, confirm it stays on one line. - Prefer clarity over cleverness; mark placeholders and TODOs clearly.
- One issue per branch; branch name starts with the issue number, e.g.
335-adding-hover-to-landing-page. - UI changes require before/after screenshots or mockups in the PR (see issue #287 as a reference).
- Run the Pa11y a11y service before submitting any UI change.
- PRs target
master.
Always apply labels when filing a GitHub issue via Claude Code (gh issue create --label ...). Pick from the existing taxonomy below — most issues warrant 2–4 labels (typically one kind + one or more areas). Run gh label list to see the current set before creating new ones; only add a new label when nothing existing fits, and keep names/casing consistent with what's there.
The taxonomy (as of June 2026):
- Kind:
Bug,New Feature,Maintenance,Code Cleanup,Data Entry,Discuss,Won't Fix,Dependencies,Security - Priority (optional, maintainer's call — don't guess):
Priority: Very High/High/Medium/Low - Effort (optional):
Easy Fix,Time Consuming - Backend / infra:
Backend,Docker,Logging,Server Start Scripts,Django Upgrade,Testing / Test Harness,Rest API,Requires Updating Model Database,Needs UW CSE IT - Frontend / UI:
UI Design,CSS,Mobile,Accessibility,Navbar,Menu Bar,Footer,Banner - Content areas / pages:
Publications,Talks,Posters,Videos,Projects,Project Gallery Page,People,Member Page,News,Awards,Landing Page,FAQ Page,Admin,Sponsors,Grants & Funding,SEO - Status:
FixedNeedsToBeTestedOnTestServer
Notes: Awards is for external recognitions and award content/data work; paper-level awards live on Publication.award (see Key model relationships) but issues about them still get Awards + Publications. Use Grants & Funding for proposals/funding-source tracking; Sponsors for sponsor logos/listings. Add Requires Updating Model Database whenever the work implies a schema/model change.