Skip to content
Draft
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
151 changes: 151 additions & 0 deletions .github/workflows/deploytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ jobs:
pnpm rebuild node-sass
- name: Build frontend
run: pnpm run build
- name: Upload frontend bundle
uses: actions/upload-artifact@v7
with:
name: studio-frontend-bundle
path: |
contentcuration/contentcuration/static/studio/
contentcuration/build/webpack-stats.json
if-no-files-found: error
retention-days: 1
make_messages:
name: Build all message files
needs: pre_job
Expand Down Expand Up @@ -79,3 +88,145 @@ jobs:
sudo apt-get install -y gettext
- name: Test Django makemessages
run: python contentcuration/manage.py makemessages --all
browser_smoke_test:
name: Browser smoke test
needs: [pre_job, build_assets]
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: learningequality
POSTGRES_PASSWORD: kolibri
POSTGRES_DB: kolibri-studio
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:6.0.9
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
env:
DJANGO_SETTINGS_MODULE: contentcuration.settings
DATA_DB_HOST: localhost
AWS_S3_ENDPOINT_URL: http://localhost:9000
AWS_BUCKET_NAME: content
CELERY_BROKER_ENDPOINT: localhost
CELERY_REDIS_DB: "0"
CELERY_REDIS_PASSWORD: ""
RUN_MODE: ci
SMOKE_EMAIL: smokeadmin@example.com
SMOKE_PASSWORD: smokepass1234
SCREENSHOT_DIR: ${{ runner.temp }}/smoke_test_screenshots
steps:
- uses: actions/checkout@v6
- name: Set up MinIO
run: |
docker run -d -p 9000:9000 --name minio \
-e "MINIO_ROOT_USER=development" \
-e "MINIO_ROOT_PASSWORD=development" \
-e "MINIO_DEFAULT_BUCKETS=content:public" \
bitnamilegacy/minio:2024.5.28
- name: Install system deps (gettext, nginx)
run: |
sudo apt-get update -y
sudo apt-get install -y gettext nginx
# nginx's postinst auto-starts the system service on the ubuntu-latest
# runner image. We launch our own nginx with a custom config below, so
# the default instance has to go first — otherwise our nginx -c fails
# on the default pid path or :80 collision.
sudo systemctl stop nginx
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: '3.10'
activate-environment: "true"
enable-cache: "true"
- name: Install python dependencies
run: uv pip sync requirements.txt
- name: Download frontend bundle
uses: actions/download-artifact@v8
with:
name: studio-frontend-bundle
- name: Cache Playwright browsers
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ runner.os }}-v1
- name: Install Chromium and system deps
run: uvx --from "playwright<2" playwright install --with-deps chromium
- name: Prepare database
run: |
python contentcuration/manage.py migrate --noinput
python contentcuration/manage.py loadconstants
- name: Create smoke test user
shell: python
run: |
import os
import sys
# contentcuration package lives one level down; put it on sys.path
# so django.setup() can import contentcuration.settings.
sys.path.insert(0, "contentcuration")
import django
django.setup()
from django.contrib.auth import get_user_model
# Studio's custom User model: create_superuser(email, first_name, last_name, password).
# is_active defaults to False, so we have to flip it explicitly —
# otherwise the user exists but can't log in.
u = get_user_model().objects.create_superuser(
os.environ["SMOKE_EMAIL"],
"Smoke",
"Admin",
password=os.environ["SMOKE_PASSWORD"],
)
u.is_active = True
u.save()
- name: Prepare static and translations
run: |
python contentcuration/manage.py collectstatic --noinput
cd contentcuration && python manage.py compilemessages
- name: Start gunicorn
run: |
cd contentcuration && \
nohup gunicorn contentcuration.wsgi:application \
--timeout=120 --workers=1 --threads=1 \
--bind=0.0.0.0:8081 --log-level=info \
> "${{ runner.temp }}/gunicorn.log" 2>&1 &
echo "gunicorn started in background"
- name: Start nginx (serves /static/, proxies / to gunicorn)
run: |
# Substitute the static dir into the checked-in template. Escape
# sed replacement metacharacters (& and \) so paths containing them
# don't corrupt the substitution.
STATIC_DIR="$GITHUB_WORKSPACE/contentcuration/static"
ESCAPED=$(printf '%s' "$STATIC_DIR" | sed -e 's/[\\&]/\\&/g')
NGINX_CONF="${{ runner.temp }}/smoke-nginx.conf"
sed "s|__STATIC_DIR__|$ESCAPED|" integration_testing/nginx-smoke.conf > "$NGINX_CONF"
sudo nginx -c "$NGINX_CONF" -g 'daemon on;'
- name: Run browser smoke test
run: uv run --script integration_testing/smoke_test.py
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: smoke_test_screenshots
path: ${{ runner.temp }}/smoke_test_screenshots
if-no-files-found: ignore
- name: Upload gunicorn log on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: smoke_test_gunicorn_log
path: ${{ runner.temp }}/gunicorn.log
if-no-files-found: ignore
176 changes: 176 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<!-- Generic guidance for all coding agents (Claude Code, Zed, Cursor, etc.) -->

# Kolibri Studio Development Guide for AI Coding Agents

**Project:** Kolibri Studio — web app for authoring and publishing learning channels to Kolibri
**Stack:** Python/Django backend, Vue.js 2.7 frontend, Kolibri Design System (KDS) + legacy Vuetify 1.5, Postgres/Redis/MinIO/Celery services, pytest/Jest testing
**Platform:** web (server-deployed, not packaged for clients)

## Quick Start

```bash
uv pip sync requirements.txt requirements-dev.txt # Python deps
pnpm install # Node deps
pre-commit install # Required — commits fail without this
make dcservicesup # Bring up postgres / redis / minio in docker
pnpm devsetup # Migrate + load sample data + create admin
pnpm devserver # Django :8080 + Webpack watcher
```

→ Full setup: `README.md` | Local production-shape stack: `make dcup` (uses `docker-compose.yml`)

## Critical Gotchas

### ⚠️ BEFORE Writing Any Vue Component, Search for Existing Ones

Do not create a new component without first searching for an existing solution:
1. **Kolibri Design System** ([docs](https://design-system.learningequality.org/)) — `KButton`, `KCircularLoader`, `KTextbox`, `KSelect`, `KModal`, `KCheckbox`, `KIcon`, `KTable`, etc.
2. **`contentcuration/contentcuration/frontend/shared/`** — Studio-specific shared components.
3. **Vuetify 1.5** — for legacy widgets KDS doesn't cover (data tables, complex layout primitives). See the next gotcha before reaching for Vuetify in new code.

If a component does 80% of what you need, wrap it — do not rewrite.

### ⚠️ Use KDS, Not Vuetify, for New Code

KDS is the design-system source of truth. Vuetify is legacy and being phased out. The rule applies to **new files / brand-new components** — if you're working inside a file that already uses Vuetify, match it (see "Match the surrounding file's API style" below) rather than mixing the two. **Do not introduce Vuetify into a fresh component.** When you encounter Vuetify being used where a KDS equivalent exists, leave it alone unless the work is genuinely about replacing it — opportunistic refactors in unrelated PRs add review noise.

### ⚠️ Use Theme Tokens, Not Hard-Coded Colors

Never use raw color values. Access theme colors via `$themeTokens` and `$themePalette`:
```vue
<template>
<div :style="{ color: $themeTokens.text, backgroundColor: $themeTokens.surface }">
<span :style="{ color: $themeTokens.annotation }">secondary text</span>
</div>
</template>
```
For computed dynamic styles, use `$computedClass`.

### ⚠️ Style Blocks, Not Inline — RTL Depends On It

Non-dynamic styles go in `<style>` blocks. RTLCSS auto-flips directional properties (`padding-left` → `padding-right`) in style blocks but **cannot flip inline styles**. Dynamic directional styles must check `isRtl`.

### ⚠️ Prefer Composition API for New Code

Studio's existing code is largely Options API, but new components and refactors should use Composition API. It's fine to add Composition API to an existing Options API file when the existing logic isn't worth restructuring — be aware that a partial mix usually leads to a follow-up refactor.

### ⚠️ No New Vuex — Use Composition API for New State

Vuex 3 is deprecated in Studio. Existing Vuex modules (under `frontend/<app>/vuex/`, including the IndexedDB-backed sync store) stay where they are; touch them as little as possible. **New state goes through Composition API** — composables built on `ref`/`reactive`/`computed`, scoped to the consuming component or a `provide`/`inject` boundary.

### ⚠️ Studio Uses an IndexedDB-Backed Change-Sync Architecture — Don't Mutate Synced Models Directly

Edits in the editor write to Dexie tables in the browser via the Resource layer (`contentcuration/contentcuration/frontend/shared/data/`), which generates Change records that flow to the server's `/api/sync/` endpoint (`contentcuration/contentcuration/viewsets/sync/`). The server applies changes via a change-type registry (`viewsets/sync/base.py`, `viewsets/sync/constants.py`) and broadcasts back.

Direct `axios.post` or direct ORM `save()` on a synced model bypasses the change pipeline and corrupts the offline → online merge. **Rule:** if a model is part of the sync framework, all writes go through the Resource layer (frontend) or the change-application registry (backend). Backend system-internal operations (publishing, garbage collection) may bypass; anything user-facing must not. See `CLAUDE.md` for a fuller description.

### ⚠️ Use Vuetify's Grid for Responsive Layout

`v-container` / `v-row` / `v-col` with Vuetify breakpoints (`xs`/`sm`/`md`/`lg`/`xl`). Studio does not have a `responsive-window` equivalent.

### ⚠️ Internationalize All User-Visible Text

Use `createTranslator` from `kolibri-i18n` — never hard-code strings in templates:
```javascript
const strings = createTranslator('ChannelStrings', {
title: { message: 'Channel title', context: 'Form field label' },
});
const { title$ } = strings; // title$() returns translated string
```

### ⚠️ API Calls via the Resource Pattern

For synced models, use the Resource layer in `contentcuration/contentcuration/frontend/shared/data/` (see sync gotcha above). For non-synced endpoints, use the existing Resource-style wrappers in `shared/data/resources.js`. Never use raw `fetch` or `axios` for domain operations.

### ⚠️ Backend APIs: Use `ValuesViewset`

Studio has `ValuesViewset` / `ReadOnlyValuesViewset` vendored at `contentcuration/contentcuration/viewsets/base.py`. Use them for new API endpoints — define a `values` tuple and `annotate_queryset` for computed fields rather than relying on default serializer output. Apply permission classes from `contentcuration/contentcuration/viewsets/`.

### ⚠️ Testing Is Required

- **Python:** `pytest` from repo root (uses `pytest.ini` → `contentcuration.test_settings`). Django API tests extend `APITestCase` from `rest_framework.test`. Other Django tests extend `django.test.TestCase`.
- **Frontend:** Jest runner + Vue Testing Library via `@testing-library/vue`. `describe`/`it`/`expect` are Jest globals — do NOT import them. Use `jest.fn()` and `jest.mock()`.
- **TDD:** Write a failing test first, then make it pass. Especially for bug fixes — always write a test that reproduces the bug before fixing it.

### ⚠️ Pre-commit Auto-Fixes Files

When a commit fails: pre-commit auto-fixes files → **`git add` the fixed files** → re-commit. Never bypass with `--no-verify`.

## Project Structure

```
studio/
├── contentcuration/ # Django project root (also contains other apps)
│ ├── contentcuration/ # Main Django app
│ │ ├── frontend/ # Vue source
│ │ │ ├── channelEdit/
│ │ │ ├── channelList/
│ │ │ ├── settings/
│ │ │ ├── accounts/
│ │ │ ├── administration/
│ │ │ └── shared/ # Shared components + sync data layer
│ │ ├── viewsets/ # DRF ValuesViewsets — sync/ contains the change framework
│ │ ├── models.py, urls.py, settings.py, dev_settings.py, test_settings.py
│ │ └── static/studio/ # webpack output (git-ignored)
│ ├── automation/ # Django app — automation workflows
│ ├── kolibri_content/ # Django app — Kolibri content schema
│ ├── kolibri_public/ # Django app — public catalog API
│ ├── search/ # Django app — full-text search
│ ├── manage.py
│ └── build/ # webpack-stats.json (git-ignored)
├── docker/ # Dockerfile.{dev,prod,nginx.prod,postgres.dev}
├── integration_testing/
│ ├── features/ # Gherkin BDD specs (manual reference)
│ └── smoke_test.py # CI smoke test
├── jest_config/ # Jest config
├── webpack.config.js
├── Makefile # dc* targets + altprodserver
└── docker-compose.yml / docker-compose.prod.yml
```

**Settings split:**
- `contentcuration.dev_settings` — local development. Use this for local `manage.py` commands.
- `contentcuration.settings` — production. Used by `make altprodserver` and the CI smoke test.
- `contentcuration.test_settings` — pytest.

## Code Quality

Studio follows the same code-quality principles as Kolibri. → See https://kolibri-dev.readthedocs.io/en/latest/code_quality.html.md for detailed examples (LLM-friendly Markdown version; drop `.md` for the HTML rendering).

## Key Conventions

**Python:** F-strings preferred. One import per line. All imports at file top — inline imports only to prevent circular imports. Descriptive migration names (no `_auto_`).

**Vue:** PascalCase filenames. Component `name` must match filename.

**Git:** Imperative commit messages. **PR titles do NOT use Conventional Commits prefix** (e.g. `feat:`, `fix:`) — plain English titles. Individual commit messages may use CC prefixes. Black/Prettier enforced by pre-commit.

**Don't guess — look at existing code** for patterns: `contentcuration/contentcuration/viewsets/` for API patterns, `contentcuration/contentcuration/frontend/shared/data/` for sync framework, existing `__tests__/` directories for test patterns.

## Running Tests

```bash
pytest # all Python tests
pytest contentcuration/contentcuration/tests/ -k name # filter by name
pnpm test # all Jest tests
pnpm jest --config jest_config/jest.conf.js <path> # single file
pre-commit run --all-files # lint all
pre-commit run --files path/to/File.vue # lint specific files
```

Important: bare `pnpm jest` will NOT load `modulePaths` correctly — always go through `pnpm test` or pass `--config jest_config/jest.conf.js` explicitly. Always go through `pre-commit` — do not invoke ESLint / Black / Flake8 directly.

## Docs Reference

The Kolibri-dev docs site serves an LLM-friendly Markdown variant of every page — append `.md` to any URL below. The whole index is at https://kolibri-dev.readthedocs.io/en/latest/llms.txt.

- Code quality: https://kolibri-dev.readthedocs.io/en/latest/code_quality.html.md
- Testing (general): https://kolibri-dev.readthedocs.io/en/latest/testing.html.md
- Frontend testing: https://kolibri-dev.readthedocs.io/en/latest/frontend_architecture/unit_testing.html.md
- Backend testing: https://kolibri-dev.readthedocs.io/en/latest/backend_architecture/testing.html.md
- i18n: https://kolibri-dev.readthedocs.io/en/latest/i18n.html.md
- Frontend architecture: https://kolibri-dev.readthedocs.io/en/latest/frontend_architecture/index.html.md
- Backend architecture: https://kolibri-dev.readthedocs.io/en/latest/backend_architecture/index.html.md
- Development workflow: https://kolibri-dev.readthedocs.io/en/latest/development_workflow.html.md

Local: `README.md`, `Makefile` (common commands), `docker-compose.yml` (service config).
Loading
Loading