This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is Kitsune, the Django platform that powers SuMo (support.mozilla.org). The project uses Docker for development.
# Initial setup (creates .env, builds images, installs dependencies)
make init # Run bootstrap script, migrations, node setup
make build # Build Docker images
# Run the application
make start # Start all services via docker-compose
# Development shells
make shell # Bash shell in container
make runshell # Bash shell with ports bound
make djshell # Django shell (ipython)
make dbshell # PostgreSQL shell# Python tests (via Docker - recommended)
make test # Run Django unit tests via ./bin/run-unit-tests.sh
make djshell # Django shell for interactive testing
# Python tests (direct commands - Docker container only)
python manage.py test # Direct Django test command (inside container)
python manage.py test path.to.specific.test --verbosity=2
# Python tests (with uv venv - local development)
uv run python manage.py test # Run all tests in uv environment
uv run python manage.py test path.to.specific.test # Run specific test
uv run python manage.py test --verbosity=2 --keepdb # Verbose output, keep test DB
# JavaScript tests
make test-js # Run JS tests via npm run webpack:test
# Lint and format
make lint # Run pre-commit hooks (includes ruff)
ruff format # Format Python code with ruff (recommended)
npm run stylelint # Lint SCSS filesPrimary Applications:
wiki/- Knowledge Base articles and documentationquestions/- Support questions and answers (Q&A system)forums/- Discussion forumsusers/- User profiles, authentication, and account managementsearch/- Elasticsearch-powered search functionalitygallery/- Media management (images, videos)products/- Mozilla product definitions and topicskpi/- Metrics and analytics dashboard
Supporting Applications:
sumo/- Core utilities, base templates, middlewareflagit/- Content moderation systemmessages/- Private messaging between usersnotifications/- Event notification systemtidings/- Email notification subscription managementkbadge/- Badge system for user achievementsgroups/- User group managementdashboards/- Analytics and reporting dashboardsllm/- AI/ML features (moderation, translations)
- Backend: Django 4.2+, Python 3.11
- Package Management: uv (replaced poetry in July 2025)
- Database: PostgreSQL
- Search: Elasticsearch 9.0+
- Cache: Redis
- Task Queue: Celery with Redis broker
- Frontend: Webpack, SCSS, vanilla JavaScript + jQuery
- Testing: Django TestCase, pytest for E2E (Playwright)
- Linting: ruff (replaced flake8/black in July 2025)
Essential environment variables are defined in .env-dist. Key ones include:
DATABASE_URL- PostgreSQL connectionES_URLS- Elasticsearch endpointsREDIS_*- Redis configuration for cache and CeleryDEBUG,DEV- Development mode flags
The docker-compose.yml defines:
web- Main Django application (port 8000)postgres- Database (port 5432)elasticsearch- Search engine (port 9200)redis- Cache and message brokercelery- Background task workermailcatcher- Email testing (port 1080)
# Install dependencies
uv sync # Install all dependencies
uv sync --frozen # Install from lockfile without updates
uv add package-name # Add new dependency
uv pip install package # Install package in current environmentpython manage.py makemigrations
python manage.py migratepython manage.py es_init --migrate-writes --migrate-reads # Initialize ES
python manage.py es_reindex --count 10 --verbosity 2 # Reindex contentKitsune supports 100+ locales. Key files:
kitsune/lib/sumo_locales.py- Locale definitionslocale/- Translation files- Language-specific synonyms in
search/dictionaries/synonyms/
# Webpack commands
npm run webpack:build # Production build
npm run webpack:watch # Development with file watching
npm run build:styleguide # Generate CSS styleguide
# Browser development
npm run start # Webpack + BrowserSync
npm run browser-sync # Live reload server- Models use
ModelBasefrom sumo.models for common fields - Views often use
mobile_template()decorator for mobile templates - Search uses custom Elasticsearch Document classes in
search/documents.py - All user-facing strings should be marked for translation with
_()or_lazy() - Cache keys use app-specific prefixes (defined in individual apps)
- Python tests live in
tests/directories within each app - Use factories from
factory_boyfor test data creation - Search tests often need
@mock.patchfor Elasticsearch - E2E tests use Playwright and are in
playwright_tests/ - Tests run in isolated database with
TESTING=True
pyproject.toml- Python dependencies and ruff configurationuv.lock- Locked dependency versions (replaces poetry.lock)package.json- Node.js dependencies, build scriptswebpack.*.js- Frontend build configurationMakefile- Development commands wrapperdocker-compose.yml- Local development stack.github/dependabot.yml- Automated dependency updates for uv and npm
Kitsune uses Django's i18n URL patterns with specific routing conventions:
Primary URL Patterns:
- Knowledge Base:
/kb/(wiki articles) - Search:
/search/ - Forums:
/forums/ - Questions:
/questions/(Q&A system) - Gallery:
/gallery/(media) - Groups:
/groups/ - Users: Root level paths like
/users/ - Products: Root level paths like
/firefox/
API Endpoints:
- v1 APIs:
/api/1/{app}/(legacy) - v2 APIs:
/api/2/{app}/(current) - Mixed APIs:
/api/{app}/(users API supports both versions) - GraphQL:
/graphql(with GraphiQL interface)
Special Routes:
/1/- In-product integration endpoints/wafflejs- Feature flag JavaScriptcontribute.json- Mozilla contribution metadata
Base Model Usage:
- All models inherit from
kitsune.sumo.models.ModelBase - Provides common functionality:
objects_range(),update()methods - Use
LocaleFieldfor language/locale fields (max_length=7, uses LANGUAGE_CHOICES) - Models define
updated_column_nameproperty for date range queries
Common Model Patterns:
- Use
factory_boyfactories for test data (located in each app) - Celery tasks defined in
tasks.pyfiles within apps - API endpoints follow REST conventions in
api.pyfiles - Each app has dedicated
urls.pyand oftenurls_api.py
GroupProfile Visibility - CRITICAL:
GroupProfile has three visibility levels (PUBLIC, PRIVATE, MODERATED) that control who can see a group. Never bypass these checks:
DANGER - Privacy Leak:
# WRONG - Exposes ALL groups, ignoring visibility settings
user.groups.all() # Leaks PRIVATE groups!
profile.user.groups.all() # Leaks PRIVATE groups!
# WRONG - Direct Group queryset bypasses GroupProfile visibility
Group.objects.filter(user=some_user) # No visibility filtering!SAFE - Respects visibility:
# Correct - Use Profile.visible_group_profiles()
profile.visible_group_profiles(viewer=request.user)
# Correct - Use GroupProfile manager
GroupProfile.objects.visible(viewer).filter(group__user=some_user)
# Correct - Check individual group visibility
group_profile.can_view(request.user)Why this matters:
- Django's
User.groupsrelationship returns ALL Group objects, ignoring GroupProfile visibility - PRIVATE groups would be exposed to anyone viewing a user profile
- Search indexing would leak private group membership
- API responses would expose sensitive group information
Safe patterns implemented:
Profile.visible_group_profiles(viewer)- Get groups respecting visibilityGroupProfile.objects.visible(viewer)- Manager method for filtering- Search indexing excludes PRIVATE groups automatically
- All views use visibility-aware methods
Never:
- Use
user.groups.all()in templates, views, or API serializers - Query
Groupmodel directly when GroupProfile visibility matters - Bypass
.visible()filtering for user-facing data
Template Architecture:
- Uses Django-Jinja templating engine (not standard Django templates)
- Mobile-specific templates supported via
mobile_template()decorator - Template tags in
kitsune.sumo.templatetags.jinja_helpers - Localization: All user-facing strings use
_()or_lazy()for translation
Elasticsearch Setup:
- Version 9.0+ required
- Custom Document classes in
search/documents.py - Management commands for index operations:
es_init --migrate-writes --migrate-reads- Initializees_reindex --count 10 --verbosity 2- Reindex content
- Language-specific synonyms in
search/dictionaries/synonyms/
Caching Strategy:
- Redis-backed caching and session storage
- App-specific cache key prefixes (defined in individual apps)
- Celery uses Redis as message broker
- Cache configuration via
REDIS_*environment variables
Useful Scripts in bin/:
run-unit-tests.sh- Main test runner (used bymake test)run-web-bootstrap.sh- Initial Django setup (migrations, collectstatic)run-node-bootstrap.sh- Node.js dependency installationrun-celery-worker.sh- Background task workerrun-celery-beat.sh- Scheduled task runner
- Always format Python files after changes with
ruff format - Use
ruff checkfor linting python files - Run
make lintbefore committing (uses pre-commit hooks) - Use uv for Python package management
- Dependabot automatically updates dependencies weekly
- Do not add trailing spaces at the end of files
- Exception handling: Be specific with exception types. Avoid catching plain
Exceptionwhen the specific exception that could occur is known. For example, useModel.DoesNotExistwhen calling.get()on a Django queryset, orKeyErrorwhen accessing dictionary keys. This makes error handling more explicit and allows unexpected exceptions to be raised rather than silently caught.
Manager Methods:
- Custom manager methods should balance conciseness with explicitness
- Preferred: Concise, verb-based names following Django's queryset API patterns:
all(),filter(),exclude(),visible(),active() - Alternative: Verbose descriptive names when explicitness adds clarity (per "explicit is better than implicit")
- Choose based on context: if the method name alone isn't clear, add descriptive prefixes
- If a method returns a queryset, the name should read naturally when chained
Example:
# Preferred - concise, follows Django patterns, clear in context
GroupProfile.objects.visible(user)
Article.objects.active()
Question.objects.recent()
# Alternative - more explicit, acceptable when clarity is prioritized
GroupProfile.objects.filter_by_visibility(user)
Article.objects.get_active_articles()
Question.objects.filter_by_recent()