A website for tracking Seattle City Council legislation, built on the Councilmatic platform.
Seattle Councilmatic makes it easy to:
- Find your Seattle City Council representative
- Track legislation (bills, resolutions, ordinances)
- See upcoming council meetings and agendas
- Follow votes on issues you care about
Live site: http://localhost:8000 (development)
┌─────────────────┐
│ seattle.gov │ Official city council website
│ legistar.com │ Legislative management system
└────────┬────────┘
│ Scrapers pull data
▼
┌─────────────────┐
│ Pupa Scrapers │ Extract & Transform data
│ (seattle/) │
└────────┬────────┘
│ Writes to OCD format
▼
┌─────────────────┐
│ PostgreSQL │ Open Civic Data models
│ + PostGIS │ (opencivicdata_*)
└────────┬────────┘
│ Synced to
▼
┌─────────────────┐
│ Councilmatic │ Django models
│ Models │ (councilmatic_core_*)
└────────┬────────┘
│ Indexed by
▼
┌─────────────────┐
│ Elasticsearch │ Full-text search
└────────┬────────┘
│ Powers
▼
┌─────────────────┐
│ Django Web UI │ User-facing website
│ (seattle_app/) │
└─────────────────┘
seattle-councilmatic/
├── seattle/ # Pupa scrapers (data collection)
│ ├── __init__.py # Jurisdiction definition
│ ├── people.py # Council member scraper
│ ├── events.py # Meeting scraper (TODO)
│ └── bills.py # Legislation scraper (TODO)
├── seattle_app/ # Django application (presentation)
│ ├── management/ # Custom management commands
│ ├── templates/ # HTML templates
│ ├── static/ # CSS, JS, images
│ ├── models.py # Custom model extensions
│ └── settings.py # Django configuration
├── scripts/ # Helper scripts
│ └── update_seattle.sh # One-command data update
├── docker-compose.yml # Container orchestration
├── Dockerfile # Container definition
└── requirements.txt # Python dependencies
- Docker Desktop
- Git
- Clone the repository:
git clone [your-repo-url]
cd seattle-councilmatic- Configure environment:
cp .env.example .env
# Edit .env and add your DJANGO_SECRET_KEY
# Generate one with: python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'- Build containers:
docker compose build- Initialize database:
# Run Django migrations
docker compose run --rm app python manage.py migrate
# Initialize Pupa tables
docker compose run --rm app pupa dbinit us
# Create admin user
docker compose run --rm app python manage.py createsuperuser- Load initial data:
docker compose run --rm app ./scripts/update_seattle.sh- Start the application:
docker compose up- Visit the site:
- Main site: http://localhost:8000
- Admin: http://localhost:8000/admin
# Full update (all scrapers)
docker compose run --rm app ./scripts/update_seattle.sh
# Update specific scraper
docker compose run --rm app ./scripts/update_seattle.sh people# All services
docker compose logs -f
# Specific service
docker compose logs -f app
docker compose logs -f webpack# Run any Django command
docker compose run --rm app python manage.py [command]
# Examples:
docker compose run --rm app python manage.py shell
docker compose run --rm app python manage.py dbshell
docker compose run --rm app python manage.py sync_councilmatic# Restart all
docker compose restart
# Restart specific service
docker compose restart app-
Scraping (
pupa update seattle)- Scrapers in
seattle/fetch data from source websites - Data is validated and cached locally
- Written to OpenCivicData tables (
opencivicdata_*)
- Scrapers in
-
Syncing (
python manage.py sync_councilmatic)- Bridges OCD models → Councilmatic models
- Handles multi-table inheritance pattern
- Creates required fields (slugs, etc.)
-
Indexing (
python manage.py update_index)- Populates Elasticsearch for full-text search
- Powers the site's search functionality
Django-councilmatic 5.x uses multi-table inheritance rather than proxy models:
- OCD Models (
opencivicdata_person): Canonical data from scrapers - Councilmatic Models (
councilmatic_core_person): Extended with web-specific fields (slugs, headshots, etc.)
They're linked via foreign key, requiring the sync step.
In seattle_app/settings.py:
# Must match your Jurisdiction.name in seattle/__init__.py
OCD_CITY_COUNCIL_NAME = 'Seattle City Council'
# Both scraper and app must be in INSTALLED_APPS
INSTALLED_APPS = [
# ...
'opencivicdata.core.apps.BaseConfig',
'councilmatic_core',
'seattle_app', # Django app
'seattle', # Pupa scrapers
]docker compose run --rm app python manage.py test# Scrape without importing (for testing)
docker compose run --rm app pupa update seattle people --scrape
# Check what was scraped
docker compose run --rm app ls -la _data/docker compose run --rm app python manage.py shellfrom opencivicdata.core.models import Person, Organization
from councilmatic_core.models import Person as CouncilPerson
# Check data counts
print(f"OCD People: {Person.objects.count()}")
print(f"Councilmatic People: {CouncilPerson.objects.count()}")
print(f"Organizations: {Organization.objects.count()}")
# Verify memberships
council = Organization.objects.get(name='Seattle City Council')
print(f"Council members: {council.memberships.count()}")- Check if data exists in database:
docker compose run --rm app python manage.py shell -c "from opencivicdata.core.models import Person; print(Person.objects.count())"- Run sync command:
docker compose run --rm app python manage.py sync_councilmatic- Rebuild search index:
docker compose run --rm app python manage.py rebuild_index --noinputCheck port conflicts:
# Check if ports are in use
lsof -i :8000 # Django
lsof -i :5432 # PostgreSQL
lsof -i :9200 # Elasticsearch
lsof -i :3000 # WebpackIncrease Docker Desktop memory allocation:
- Settings → Resources → Memory → Set to at least 4GB
Reset and re-initialize:
docker compose down -v # WARNING: Deletes all data!
docker compose up -d postgres
docker compose run --rm app python manage.py migrate
docker compose run --rm app pupa dbinit us- Councilmatic Website
- Open Civic Data & Pupa Documentation
- Open Civic Data Github
- Django Documentation
-
Create scraper file in
seattle/(e.g.,events.py) -
Register in jurisdiction (
seattle/__init__.py):
from .events import SeattleEventScraper
class Seattle(Jurisdiction):
scrapers = {
"people": SeattlePersonScraper,
"events": SeattleEventScraper, # Add here
}- Test the scraper:
docker compose run --rm app pupa update seattle events --scrape- Run full import:
docker compose run --rm app ./scripts/update_seattle.sh events- Follow PEP 8 for Python
- Use Django conventions
- Add docstrings to functions
- Comment complex logic
[Your chosen license]
Built with:
- Councilmatic by DataMade
- Open Civic Data
- Pupa scraping framework
Data sources: