Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a1f31b1
Add Django project entrypoint
dragosandreibobu Jul 16, 2026
b8302f8
Configure Django for Supabase Postgres
dragosandreibobu Jul 16, 2026
fb2ab05
Add admin and graph API routes
dragosandreibobu Jul 16, 2026
a52de8d
Add Django WSGI application
dragosandreibobu Jul 16, 2026
c236119
Model concepts and typed relations
dragosandreibobu Jul 16, 2026
cc491e2
Add graph serializers
dragosandreibobu Jul 16, 2026
a5d5ab7
Expose concept relation and graph endpoints
dragosandreibobu Jul 16, 2026
8ec8729
Add graph and guarded SQL admin tools
dragosandreibobu Jul 16, 2026
47ab9b1
Add interactive graph admin view
dragosandreibobu Jul 16, 2026
fd2be2c
Add SQL workbench admin view
dragosandreibobu Jul 16, 2026
7f79cd4
Replace FastAPI dependencies with Django stack
dragosandreibobu Jul 16, 2026
a45dc57
Add graph Django app configuration
dragosandreibobu Jul 16, 2026
fb054e0
Add initial concept graph schema
dragosandreibobu Jul 16, 2026
eeadb22
Wire custom graph administration site
dragosandreibobu Jul 16, 2026
c126f45
Initialize Django config package
dragosandreibobu Jul 16, 2026
03c91eb
Initialize graph app package
dragosandreibobu Jul 16, 2026
2904204
Initialize graph migrations package
dragosandreibobu Jul 16, 2026
a4644d4
Match Django models to Supabase UUID schema
dragosandreibobu Jul 16, 2026
9127bc4
Adopt and extend existing Supabase graph tables
dragosandreibobu Jul 16, 2026
eb0d5c9
Allow extensible relation vocabulary
dragosandreibobu Jul 16, 2026
e4025c6
Refresh Django dependencies
dragosandreibobu Jul 16, 2026
65c94e9
Harden Django configuration
dragosandreibobu Jul 16, 2026
ad61ec3
Add bounded graph query service
dragosandreibobu Jul 16, 2026
28edcc4
Secure graph and SQL admin tools
dragosandreibobu Jul 16, 2026
3a1a7cd
Add filtered graph and health endpoints
dragosandreibobu Jul 16, 2026
a771a34
Expose API health endpoint
dragosandreibobu Jul 16, 2026
d550212
Make graph visualization safe and filterable
dragosandreibobu Jul 16, 2026
3ee51fc
Add guarded SQL workbench controls
dragosandreibobu Jul 16, 2026
02eb216
Add admin graph tool shortcuts
dragosandreibobu Jul 16, 2026
2a4c409
Replace FastAPI tests with Django coverage
dragosandreibobu Jul 16, 2026
8654605
Configure Django test discovery
dragosandreibobu Jul 16, 2026
adb1e6e
Add Django PostgreSQL CI workflow
dragosandreibobu Jul 16, 2026
49c21e4
Document Django graph API
dragosandreibobu Jul 16, 2026
45ebde1
Document Django environment variables
dragosandreibobu Jul 16, 2026
90b0417
Remove obsolete FastAPI application
dragosandreibobu Jul 16, 2026
48dff32
Remove obsolete FastAPI application
dragosandreibobu Jul 16, 2026
6d38ce4
Remove obsolete FastAPI application
dragosandreibobu Jul 16, 2026
14c2d19
Remove obsolete FastAPI application
dragosandreibobu Jul 16, 2026
bef0bc9
Add API agent instructions
dragosandreibobu Jul 16, 2026
42978bb
Align initial migration state with relation model
dragosandreibobu Jul 16, 2026
bf346ae
Require Django model permissions for API writes
dragosandreibobu Jul 16, 2026
a7c0f47
Test model-permission API writes
dragosandreibobu Jul 16, 2026
5c375fa
Trigger Vercel redeployment
dragosandreibobu Jul 17, 2026
3a256b5
Remove Vercel redeploy marker
dragosandreibobu Jul 17, 2026
33e3824
Trigger Vercel redeployment
dragosandreibobu Jul 17, 2026
42ca0e2
Remove Vercel redeploy marker
dragosandreibobu Jul 17, 2026
8c968d5
Harden Django validation and Vercel deployment
dragosandreibobu Jul 19, 2026
2da0c39
Separate Vercel build and runtime settings
dragosandreibobu Jul 19, 2026
a5d3166
Include psycopg runtime typing dependency
dragosandreibobu Jul 19, 2026
2eefc6f
Use Vercel-compatible PostgreSQL driver
dragosandreibobu Jul 19, 2026
d5a2b3c
Run tests with development transport settings
dragosandreibobu Jul 19, 2026
d8eacb9
Use non-manifest static storage in tests
dragosandreibobu Jul 19, 2026
b28e57a
Use locked Psycopg 3 dependencies
dragosandreibobu Jul 19, 2026
64212ad
Pin Psycopg runtime typing dependency
dragosandreibobu Jul 19, 2026
b80c43c
Pin Vercel Python runtime to 3.12
dragosandreibobu Jul 19, 2026
32c72a2
Use supported Vercel Python detection
dragosandreibobu Jul 19, 2026
fc8bb9e
Expose collected static output to Vercel
dragosandreibobu Jul 19, 2026
31dabc6
Use Vercel zero-config Django runtime
dragosandreibobu Jul 19, 2026
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
10 changes: 8 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
# FastAPI app settings
APP_ENV=development
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/curricula
DB_SSL_REQUIRE=false
DJANGO_SECRET_KEY=replace-me
DJANGO_DEBUG=true
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8000
# Set automatically by Vercel; documented here because it changes DB pooling behavior.
VERCEL=
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: API CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_DB: curricula_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d curricula_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/curricula_test
DB_SSL_REQUIRE: "false"
DJANGO_SECRET_KEY: ci-only-secret-key-with-more-than-fifty-characters-123456789
DJANGO_ALLOWED_HOSTS: localhost,127.0.0.1
DJANGO_DEBUG: "true"
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: pip
- name: Install dependencies
run: python -m pip install --upgrade pip && pip install -r requirements.txt
- name: Compile Python sources
run: python -m compileall -q config graph tests
- name: Apply migrations
run: python manage.py migrate --noinput
- name: Check Django configuration and migrations
run: |
python manage.py check
python manage.py check --deploy
python manage.py makemigrations --check --dry-run
- name: Run tests
run: pytest
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ cover/
local_settings.py
db.sqlite3
db.sqlite3-journal
staticfiles/

# Flask stuff:
instance/
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
33 changes: 33 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Agent instructions

## Scope

This repository is the Django API and administration surface for the curricula.live knowledge graph.

## Invariants

- PostgreSQL/Supabase is the target database. Do not replace it with SQLite-specific behavior.
- Preserve the existing `concept` and `relation` table names and UUID primary keys.
- Preserve relation foreign-key column names `source` and `target`.
- Relation predicates are extensible lowercase snake_case strings, not a closed enum.
- Public graph responses must remain bounded; do not return the entire graph by default.
- The SQL workbench must remain superuser-only, single-statement, timeout-bounded, and read-only unless write mode is explicitly confirmed.
- Never commit database credentials, service-role keys, or production secrets.

## Validation

Run before proposing changes:

```bash
python manage.py check
python manage.py makemigrations --check --dry-run
pytest
```

CI provisions PostgreSQL 17 and applies the real migrations.

## Preferred changes

- Add tests with every behavior change.
- Keep API and admin graph filtering logic in `graph/services.py` so both surfaces behave consistently.
- Use Django JSON serialization helpers in templates; never inject Python representations with `|safe`.
58 changes: 53 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,71 @@
# curricula.live API

Minimal FastAPI starter for the curricula.live API.
Django and Django REST Framework service for editing and serving the curricula.live concept graph stored in PostgreSQL/Supabase.

## Setup
## Features

- REST resources for concepts and typed relations
- bounded graph endpoint with text and relation-type filters
- Django admin tables with search and relation autocomplete
- graph explorer at `/admin/graph/`
- superuser-only SQL workbench at `/admin/sql/`

## Local setup

The project targets PostgreSQL because its initial migration adopts the existing Supabase UUID schema.

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
```

## Run locally
Export the variables from `.env`, then run:

```bash
uvicorn app.main:app --reload
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
```

## Test
## API

- `GET /api/health/`
- `GET /api/concepts/`
- `GET /api/relations/`
- `GET /api/graph/?q=queue&type=prerequisite_of&limit=250`

Anonymous clients may read. Authenticated Django users may write through the REST API according to their permissions.

## Tests

```bash
pytest
```

The GitHub Actions workflow provisions PostgreSQL 17, applies migrations, checks for migration drift, and runs the test suite.

## Deployment

Vercel runs `config.wsgi:application` as a serverless Python function and
collects static assets during the build. Do not use the deployment filesystem
for persistent data or run migrations during function startup.

Configure these separately for Preview and Production:

- `DATABASE_URL` (required): PostgreSQL connection URL. Preview must use a
staging database or a database role that cannot modify production data.
- `DJANGO_SECRET_KEY` (required): a distinct, random value per environment.
- `DJANGO_DEBUG` (required): `false` outside local development.
- `DJANGO_ALLOWED_HOSTS` (required): comma-separated deployment hostnames.
- `DJANGO_CSRF_TRUSTED_ORIGINS` (required for admin/API browser writes):
comma-separated `https://` origins.
- `DB_SSL_REQUIRE` (required for Supabase): `true`.

Run migrations as an explicit, reviewed deployment operation. The current
initial migration adopts and extends the graph tables, so it must not be run
against production until its SQL has been compared with the real Supabase
schema and tested on a staging clone. Create the first administrator with
`python manage.py createsuperuser` from a trusted one-off environment connected
to the intended database.
1 change: 0 additions & 1 deletion app/__init__.py

This file was deleted.

11 changes: 0 additions & 11 deletions app/main.py

This file was deleted.

19 changes: 0 additions & 19 deletions app/routes/health.py

This file was deleted.

File renamed without changes.
117 changes: 117 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import os
from pathlib import Path

import dj_database_url
from django.core.exceptions import ImproperlyConfigured

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-only-change-me")
DEBUG = os.getenv("DJANGO_DEBUG", "false").lower() == "true"
if not DEBUG and SECRET_KEY == "dev-only-change-me":
raise ImproperlyConfigured("DJANGO_SECRET_KEY is required when DJANGO_DEBUG is false.")
ALLOWED_HOSTS = [
host.strip()
for host in os.getenv("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
if host.strip()
]
CSRF_TRUSTED_ORIGINS = [
origin.strip()
for origin in os.getenv("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",")
if origin.strip()
]

INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"whitenoise.runserver_nostatic",
"django_filters",
"rest_framework",
"graph",
]

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
}
]
WSGI_APPLICATION = "config.wsgi.application"

DATABASES = {
"default": dj_database_url.config(
default=os.getenv(
"DATABASE_URL",
"postgresql://postgres:postgres@localhost:5432/curricula",
),
conn_max_age=0 if os.getenv("VERCEL") else 60,
conn_health_checks=True,
ssl_require=os.getenv("DB_SSL_REQUIRE", "false").lower() == "true",
)
}

AUTH_PASSWORD_VALIDATORS = []
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Europe/Bucharest"
USE_I18N = True
USE_TZ = True

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {
"BACKEND": (
"django.contrib.staticfiles.storage.StaticFilesStorage"
if DEBUG
else "whitenoise.storage.CompressedStaticFilesStorage"
)
},
}
WHITENOISE_USE_FINDERS = True
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
SECURE_SSL_REDIRECT = not DEBUG
SECURE_HSTS_SECONDS = 31536000 if not DEBUG else 0
SECURE_HSTS_INCLUDE_SUBDOMAINS = not DEBUG
SECURE_HSTS_PRELOAD = not DEBUG

REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly"
],
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 100,
}
22 changes: 22 additions & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter

from graph.admin import ConceptAdmin, GraphAdminSite, RelationAdmin
from graph.models import Concept, Relation
from graph.views import ConceptViewSet, RelationViewSet, graph_data, health

router = DefaultRouter()
router.register("concepts", ConceptViewSet)
router.register("relations", RelationViewSet)

graph_admin = GraphAdminSite(name="graph_admin")
graph_admin.register(Concept, ConceptAdmin)
graph_admin.register(Relation, RelationAdmin)

urlpatterns = [
path("", health, name="root-health"),
path("admin/", graph_admin.urls),
path("api/", include(router.urls)),
path("api/health/", health, name="health"),
path("api/graph/", graph_data, name="graph-data"),
]
4 changes: 4 additions & 0 deletions config/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()
Empty file added graph/__init__.py
Empty file.
Loading