QuakerCMS is a Wagtail-based CMS for Quaker communities built with Django 5.2+ and Python 3.12+. It uses uv for package management and emphasizes runtime-configurable internationalization.
ALL language/i18n constants MUST be defined in src/core/constants.py, never in settings or models directly:
# ✅ CORRECT - Import from core.constants
from core.constants import DEFAULT_LANGUAGE_CODE, LANGUAGE_CHOICES
# ❌ WRONG - Don't define in settings or access via settings
settings.DEFAULT_LANGUAGE_CODE # This doesn't exist!Why: Settings are loaded once at startup. Constants in core/constants.py are the single source of truth for language configuration, used by:
src/core/settings/base.py- Django settingssrc/locales/models.py- Model field choicessrc/locales/utils.py- Runtime utilities
Languages are configured at runtime via LocaleSettings model in Wagtail admin, NOT in code:
LocaleSettings.save()auto-createsLocalerecords (no manual sync needed in normal flow)- Validation prevents deleting locales with content (shows model breakdown)
- Use
python manage.py sync_locales --remove-unusedto clean up unused locales - Settings require server restart to take effect (Django loads at startup)
Custom blocks use StructBlock with templates for semantic control:
# Example: HeadingBlock restricts to h2-h4 (h1 reserved for page title)
class HeadingBlock(blocks.StructBlock):
text = blocks.CharBlock(required=True, help_text="The heading text")
level = blocks.ChoiceBlock(
choices=[("h2", "Heading 2"), ("h3", "Heading 3"), ("h4", "Heading 4")],
default="h2",
)
class Meta:
template = "content/blocks/heading_block.html"Security: Use EmbedBlock (oEmbed) for embedded content, NEVER RawHTMLBlock (XSS risk).
Navigation Menu Pattern: See src/navigation/blocks.py for nested block structure that enforces 2-level maximum:
TopLevelMenuBlock(StreamBlock) contains page_link, external_link, OR dropdownDropdownMenuBlock(StructBlock) contains title and items (MenuItemBlock)MenuItemBlock(StreamBlock) contains ONLY page_link and external_link (no nested dropdowns)
This structural approach prevents 3+ level nesting at the schema level.
- Translation UI comes from
wagtail.contrib.simple_translation(already installed) - Content is per-locale, not auto-translated
- Use
copy_for_translation()to create locale variants - Management commands:
show_language_settings- Display current config (DB + settings + constants)sync_locales- Sync Locale model with LocaleSettings (rarely needed)
IMPORTANT: This project uses uv for package management. When running Python commands in terminals (especially from AI assistants or automated tools), the virtual environment may not be automatically activated. Always prefix Python commands with uv run to ensure the correct environment is used:
# ✅ CORRECT - Use uv run for all Python commands
uv run python manage.py test
uv run python manage.py migrate
uv run python manage.py runserver
# ❌ WRONG - May fail if virtual environment isn't activated
python manage.py testuv sync # Install all deps (creates .venv/)
uv add package-name # Add runtime dependency
uv add --dev package-name # Add dev dependencycd src
uv run python manage.py migrate
uv run python manage.py test # Run all tests
uv run python manage.py test navigation # Run specific app tests
uv run python manage.py createsuperuser # One-time setup
uv run python manage.py runserver # Development serveruv run pre-commit install # One-time setup
uv run pre-commit run --all-files # Manual run
uv run ruff check --fix . # Lint with auto-fix
uv run ruff format . # Format codeUse descriptive test classes grouped by feature:
ModelTests- Model behavior and validationStreamFieldTests- StreamField block testingTranslationTests- Locale/i18n functionalityAdminTests- Admin interface integrationIntegrationTests- Cross-app functionality
Example test pattern:
def test_heading_block_supports_multiple_levels(self):
"""Test that heading block supports h2, h3, and h4 levels."""
content_page = ContentPage(
# ... setup ...
body=[
{"type": "heading", "value": {"text": "Main", "level": "h2"}},
{"type": "heading", "value": {"text": "Sub", "level": "h3"}},
],
)
# Verify levels
self.assertEqual(content_page.body[0].value["level"], "h2")src/
├── core/ # Django settings + centralized constants
│ ├── constants.py # ⭐ ALL i18n constants defined here
│ └── settings/
│ ├── base.py # Imports from core.constants
│ ├── dev.py # Development settings
│ └── production.py # Production settings
├── locales/ # Runtime language configuration
│ ├── models.py # LocaleSettings with auto-sync
│ ├── utils.py # get_language_settings() helper
│ └── management/commands/
│ ├── show_language_settings.py
│ └── sync_locales.py
├── content/ # General content pages
│ ├── models.py # ContentPage with HeadingBlock
│ └── templates/content/blocks/
│ └── heading_block.html
├── home/ # Home page app
└── search/ # Search functionality
- core - Settings, constants, shared config (no models)
- locales - Runtime i18n configuration via Wagtail settings
- content - Flexible ContentPage with StreamField body
- home - HomePage model (site root)
# Don't access constants via settings
from django.conf import settings
settings.DEFAULT_LANGUAGE_CODE # AttributeError!
# Don't use RawHTMLBlock
("embed", blocks.RawHTMLBlock()) # XSS vulnerability
# Don't name blocks generically
("paragraph", blocks.RichTextBlock()) # Misleading - supports rich text# Import constants directly
from core.constants import DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGES
# Use EmbedBlock for safety
("embed", EmbedBlock()) # Safe oEmbed integration
# Name blocks accurately
("rich_text", blocks.RichTextBlock()) # Clear intent- ruff - Linting + formatting (replaces black/flake8/isort)
- pyupgrade - Modern Python syntax (3.12+)
- django-upgrade - Django 5.2+ patterns
- djhtml - Template formatting
- curlylint - Template linting
- Test migrations are auto-generated
- based on model changes
- Run
python manage.py makemigrationsafter model changes - All StreamField changes require migrations (block structure changes)
- Check migration files into version control
- Check
src/core/constants.pyhas the language code - Verify
LocaleSettingsexists in admin (Settings → Locale Settings) - Run
python manage.py show_language_settingsto debug - Restart server after changing LocaleSettings (settings load at startup)
Update test data to match new block structure:
# Old CharBlock format
{"type": "heading", "value": "My Heading"}
# New StructBlock format
{"type": "heading", "value": {"text": "My Heading", "level": "h2"}}# Run checks to see what failed
uv run pre-commit run --all-files
# Common fixes
uv run ruff format . # Fix formatting
uv run ruff check --fix . # Auto-fix linting- Wagtail Documentation - CMS framework
- Django 5.2 Documentation - Web framework
- uv Documentation - Package manager
src/locales/README.md- Detailed i18n architecture guide