Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,99 @@ jobs:

- name: Run end-to-end tests
run: python manage.py test website.tests.test_member_e2e --settings=makeabilitylab.settings_test --verbosity=2

# Accessibility sweep (Pa11y + Axe, WCAG 2.0 AA). CONTRIBUTING requires Pa11y
# on UI changes but nothing enforced it (#1278 item 6). This wires it in.
#
# Like `test` and `e2e`, it is REPORT-ONLY — it surfaces violations in the run
# Summary but never fails the build (so a pre-existing violation doesn't sit
# red next to every master deploy). Tighten to blocking later once the current
# findings are triaged.
#
# The local `.pa11yci.json` targets the docker-compose host with the
# maintainer's real DB snapshot; CI has no snapshot, so we build a fresh DB
# from models, seed deterministic demo content (seed_demo_projects +
# seed_demo_news), serve it with a native runserver, and scan the localhost
# URLs in `.pa11yci.ci.json`.
a11y:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16
env:
POSTGRES_DB: makeability
POSTGRES_USER: admin
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U admin -d makeability"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
DATABASE_HOST: localhost
DATABASE_PORT: 5432
# DJANGO_ENV=DEBUG -> DEBUG=True, so the dev runserver serves media/static
# and renders real error pages while pa11y scans.
DJANGO_ENV: DEBUG
DJANGO_SETTINGS_MODULE: makeabilitylab.settings_test

steps:
- uses: actions/checkout@v4

# ImageMagick + Ghostscript power the PDF->thumbnail path the demo
# publications hit on save; libpq-dev builds psycopg2.
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends imagemagick ghostscript libpq-dev
sudo cp imagemagick-policy.xml /etc/ImageMagick-6/policy.xml

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip

- name: Install Python dependencies
run: pip install -r requirements.txt

# Build the website schema from models (migrations are gitignored;
# settings_test sets MIGRATION_MODULES={'website': None}, so --run-syncdb
# creates the tables directly) and seed deterministic demo content.
- name: Build schema and seed demo data
run: |
python manage.py migrate --run-syncdb
python manage.py seed_demo_projects
python manage.py seed_demo_news

- name: Start dev server
run: |
python manage.py runserver 0.0.0.0:8000 --noreload &
echo "Waiting for the server to come up…"
for i in $(seq 1 30); do
if curl -sf -o /dev/null http://localhost:8000/; then
echo "Server is up."; exit 0
fi
sleep 1
done
echo "Server did not start in time"; exit 1

- name: Install pa11y-ci
run: npm install -g pa11y-ci

# Report-only: `|| true` so violations never fail the job. Output is teed
# to the log and a trimmed tail posted to the run Summary.
- name: Run Pa11y accessibility sweep
run: |
pa11y-ci --config .pa11yci.ci.json 2>&1 | tee pa11y-output.txt || true
{
echo "## Accessibility (Pa11y + Axe, WCAG2AA) — report-only"
echo
echo '```'
tail -n 60 pa11y-output.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
24 changes: 24 additions & 0 deletions .pa11yci.ci.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"defaults": {
"standard": "WCAG2AA",
"runners": ["axe", "htmlcs"],
"timeout": 60000,
"chromeLaunchConfig": {
"args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
},
"hideElements": "#js-toc",
"ignore": []
},
"_comment": "CI variant of .pa11yci.json (#1278 item 6). The local-dev config targets the docker-compose 'website:8000' host with the maintainer's real DB snapshot (jonfroehlich, sidewalk, ...). CI has no snapshot, so this config targets a native runserver on localhost:8000 seeded with deterministic demo content via `manage.py seed_demo_projects` + `seed_demo_news`. Keep the two URL lists in sync with what those seed commands create.",
"urls": [
"http://localhost:8000/",
"http://localhost:8000/people/",
"http://localhost:8000/publications/",
"http://localhost:8000/projects/",
"http://localhost:8000/news/",
"http://localhost:8000/awards/",
"http://localhost:8000/project/demo-active-tall/",
"http://localhost:8000/member/demolovelace/",
"http://localhost:8000/news/demo-news-one/"
]
}
58 changes: 58 additions & 0 deletions website/management/commands/seed_demo_news.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Local-dev / CI helper: seed a few demo news items so the news listing
(``/news/``) and a news detail page render with real content.

Run inside the website container (or in CI):

python manage.py seed_demo_news

Idempotent — deletes any prior demo news (title starting with "Demo News")
and recreates from scratch, so it's safe to re-run. Pairs with
``seed_demo_projects``: together they populate enough content for the Pa11y
accessibility sweep (#1278 item 6) to scan real pages rather than empty ones.
If demo people exist (from ``seed_demo_projects``), the first one is set as the
author so the byline path renders too.

News.save() derives the slug from the title, so the detail URLs are stable:
"Demo News One" -> /news/demo-news-one/.

This file lives in management/commands/ so Django auto-discovers it, but it's
explicitly a dev/test tool — don't wire it into docker-entrypoint.sh.
"""

from datetime import date

from django.core.management.base import BaseCommand


class Command(BaseCommand):
help = "Seed a few demo news items (for local visual testing and the Pa11y CI sweep)."

_TITLES = ["Demo News One", "Demo News Two", "Demo News Three"]

def handle(self, *args, **opts):
from website.models import News, Person

wiped = News.objects.filter(title__startswith="Demo News").count()
if wiped:
self.stdout.write(self.style.WARNING(f"Removing {wiped} prior demo news item(s)."))
News.objects.filter(title__startswith="Demo News").delete()

# Reuse a demo author if seed_demo_projects has run; otherwise authorless
# (News.author is nullable) — both paths should render.
author = Person.objects.filter(first_name="Demo").order_by("last_name").first()

self.stdout.write(self.style.NOTICE("Creating demo news items…"))
for i, title in enumerate(self._TITLES):
news = News.objects.create(
title=title,
content=(
f"<p>This is demo news item #{i + 1}, created for local visual "
f"testing and the automated accessibility (Pa11y) sweep.</p>"
),
date=date(2024, 1, i + 1),
author=author,
)
self.stdout.write(f" ✓ /news/{news.slug}/")

self.stdout.write(self.style.SUCCESS("Done."))
Loading