Skip to content

Commit 858360e

Browse files
authored
Merge pull request #48 from mfwolffe/main
DAWn_EE guided activity models
2 parents 82d2f23 + 99495eb commit 858360e

19 files changed

Lines changed: 759 additions & 6 deletions

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python 3.11.6
File renamed without changes.

Procfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
web: bash start.sh

config/api_router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
SubmissionViewSet,
1717
AttachmentViewSet,
1818
TeacherSubmissionViewSet,
19+
ActivityProgressViewSet,
1920
)
2021
from teleband.musics.api.views import PieceViewSet
2122
from teleband.instruments.api.views import InstrumentViewSet
@@ -45,6 +46,7 @@
4546

4647
assignments_router = nested_cls(courses_router, "assignments", lookup="assignment")
4748
assignments_router.register("submissions", SubmissionViewSet)
49+
assignments_router.register("activity-progress", ActivityProgressViewSet, basename="activity-progress")
4850

4951
attachments_router = nested_cls(assignments_router, "submissions", lookup="submission")
5052
attachments_router.register("attachments", AttachmentViewSet)

config/settings/railway.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""
2+
Django settings for Railway deployment.
3+
4+
Simplified production settings without AWS dependencies.
5+
Uses whitenoise for static files and local filesystem for media.
6+
"""
7+
8+
from .base import * # noqa
9+
from .base import env, ROOT_DIR
10+
import os
11+
12+
# GENERAL
13+
# ------------------------------------------------------------------------------
14+
SECRET_KEY = env("DJANGO_SECRET_KEY")
15+
DEBUG = env.bool("DJANGO_DEBUG", default=False)
16+
17+
# Allow Railway's domain and custom domains
18+
ALLOWED_HOSTS = env.list(
19+
"DJANGO_ALLOWED_HOSTS",
20+
default=["localhost", ".railway.app", ".up.railway.app"]
21+
)
22+
23+
# DATABASES
24+
# ------------------------------------------------------------------------------
25+
# Railway provides DATABASE_URL automatically when you add Postgres
26+
# Falls back to SQLite for simpler setups
27+
if env("DATABASE_URL", default=None):
28+
DATABASES = {
29+
"default": env.db("DATABASE_URL"),
30+
}
31+
DATABASES["default"]["ATOMIC_REQUESTS"] = True
32+
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60)
33+
else:
34+
DATABASES = {
35+
"default": {
36+
"ENGINE": "django.db.backends.sqlite3",
37+
"NAME": ROOT_DIR / "db.sqlite3",
38+
}
39+
}
40+
41+
# CACHES
42+
# ------------------------------------------------------------------------------
43+
# Use local memory cache instead of Redis for simplicity
44+
CACHES = {
45+
"default": {
46+
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
47+
"LOCATION": "unique-snowflake",
48+
}
49+
}
50+
51+
# SECURITY
52+
# ------------------------------------------------------------------------------
53+
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
54+
SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
55+
SESSION_COOKIE_SECURE = True
56+
CSRF_COOKIE_SECURE = True
57+
SECURE_HSTS_SECONDS = 60
58+
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
59+
SECURE_HSTS_PRELOAD = True
60+
SECURE_CONTENT_TYPE_NOSNIFF = True
61+
62+
# STATIC FILES (whitenoise)
63+
# ------------------------------------------------------------------------------
64+
INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa F405
65+
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") # After SecurityMiddleware
66+
67+
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
68+
STATIC_URL = "/static/"
69+
STATIC_ROOT = ROOT_DIR / "staticfiles"
70+
71+
# MEDIA FILES (local filesystem)
72+
# ------------------------------------------------------------------------------
73+
# Media files stored in Railway volume at /app/mediafiles
74+
# Sample audio copied from git on first deploy, user recordings persist in volume
75+
MEDIA_URL = "/media/"
76+
MEDIA_ROOT = env("MEDIA_ROOT", default="/app/mediafiles")
77+
78+
# EMAIL
79+
# ------------------------------------------------------------------------------
80+
# Use console backend for development/testing (emails print to console)
81+
EMAIL_BACKEND = env(
82+
"DJANGO_EMAIL_BACKEND",
83+
default="django.core.mail.backends.console.EmailBackend"
84+
)
85+
DEFAULT_FROM_EMAIL = env(
86+
"DJANGO_DEFAULT_FROM_EMAIL",
87+
default="MusicCPR <noreply@musiccpr.org>"
88+
)
89+
90+
# ADMIN
91+
# ------------------------------------------------------------------------------
92+
ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin/")
93+
94+
# LOGGING
95+
# ------------------------------------------------------------------------------
96+
LOGGING = {
97+
"version": 1,
98+
"disable_existing_loggers": False,
99+
"formatters": {
100+
"verbose": {
101+
"format": "%(levelname)s %(asctime)s %(name)s %(message)s"
102+
}
103+
},
104+
"handlers": {
105+
"console": {
106+
"level": "DEBUG",
107+
"class": "logging.StreamHandler",
108+
"formatter": "verbose",
109+
},
110+
},
111+
"root": {"level": "INFO", "handlers": ["console"]},
112+
"loggers": {
113+
"django": {
114+
"handlers": ["console"],
115+
"level": "WARNING",
116+
"propagate": False,
117+
},
118+
"django.request": {
119+
"handlers": ["console"],
120+
"level": "ERROR",
121+
"propagate": False,
122+
},
123+
},
124+
}
125+
126+
# CORS
127+
# ------------------------------------------------------------------------------
128+
# Allow Vercel frontend domains
129+
CORS_ALLOWED_ORIGIN_REGEXES = env.list(
130+
"CORS_ALLOWED_ORIGINS_REGEX",
131+
default=[
132+
r"^https://.*\.vercel\.app$",
133+
r"^https://.*\.railway\.app$",
134+
r"^http://localhost:\d+$",
135+
r"^http://127\.0\.0\.1:\d+$",
136+
]
137+
)
138+
139+
# Also allow specific origins if set
140+
CORS_ALLOWED_ORIGINS = env.list(
141+
"CORS_ALLOWED_ORIGINS",
142+
default=[]
143+
)
144+
145+
# CSRF trusted origins (needed for admin)
146+
CSRF_TRUSTED_ORIGINS = env.list(
147+
"CSRF_TRUSTED_ORIGINS",
148+
default=["https://*.railway.app", "https://*.vercel.app"]
149+
)

config/urls.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,51 @@
1+
import os
12
from django.conf import settings
23
from django.conf.urls.static import static
34
from django.contrib import admin
45
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
5-
from django.urls import include, path
6+
from django.urls import include, path, re_path
67
from django.views import defaults as default_views
78
from django.views.generic import TemplateView
9+
from django.views.static import serve
10+
from django.http import JsonResponse
811
from teleband.users.api.views import obtain_delete_auth_token
912
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
1013

14+
15+
def debug_media(request):
16+
"""Diagnostic endpoint to check media files."""
17+
import subprocess
18+
result = {
19+
"MEDIA_ROOT": str(settings.MEDIA_ROOT),
20+
"MEDIA_ROOT_exists": os.path.exists(settings.MEDIA_ROOT),
21+
"cwd": os.getcwd(),
22+
}
23+
24+
# List files in MEDIA_ROOT
25+
if os.path.exists(settings.MEDIA_ROOT):
26+
try:
27+
files = []
28+
for root, dirs, filenames in os.walk(settings.MEDIA_ROOT):
29+
for f in filenames[:20]: # Limit to first 20
30+
files.append(os.path.join(root, f).replace(settings.MEDIA_ROOT, ""))
31+
result["files"] = files
32+
result["file_count"] = sum(len(f) for _, _, f in os.walk(settings.MEDIA_ROOT))
33+
except Exception as e:
34+
result["error"] = str(e)
35+
else:
36+
result["files"] = []
37+
38+
# Also check teleband/media
39+
teleband_media = os.path.join(os.getcwd(), "teleband", "media")
40+
result["teleband_media_exists"] = os.path.exists(teleband_media)
41+
if os.path.exists(teleband_media):
42+
result["teleband_media_count"] = sum(len(f) for _, _, f in os.walk(teleband_media))
43+
44+
return JsonResponse(result)
45+
1146
urlpatterns = [
1247
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
48+
path("debug-media/", debug_media, name="debug-media"),
1349
path(
1450
"about/", TemplateView.as_view(template_name="pages/about.html"), name="about"
1551
),
@@ -27,7 +63,16 @@
2763
name="swagger-ui",
2864
),
2965
path("dashboards/", include("teleband.dashboards.urls", namespace="dashboards")),
30-
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
66+
]
67+
68+
# Serve media files - in production with S3 this is handled by S3,
69+
# but for Railway/local deployments we serve from filesystem
70+
# Note: static() only works with DEBUG=True, so we use serve() directly for non-S3 deployments
71+
if not hasattr(settings, 'DEFAULT_FILE_STORAGE') or 'S3' not in getattr(settings, 'DEFAULT_FILE_STORAGE', ''):
72+
urlpatterns += [
73+
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
74+
]
75+
3176
if settings.DEBUG:
3277
# Static file serving when using Gunicorn + Uvicorn for local web socket development
3378
urlpatterns += staticfiles_urlpatterns()

docs/railway-deployment.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Railway + Vercel Deployment
2+
3+
## Backend (Railway)
4+
5+
### Setup
6+
7+
1. Create project in Railway, connect GitHub repo
8+
2. Add PostgreSQL: **+ New****Database****PostgreSQL**
9+
3. Add Volume: **+ New****Volume** → mount at `/app/mediafiles`
10+
4. Set environment variables (see below)
11+
5. Enable public networking: **Settings****Networking****Public Networking**
12+
13+
### Environment Variables
14+
15+
| Variable | Value |
16+
|----------|-------|
17+
| `DJANGO_SECRET_KEY` | `python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"` |
18+
| `MEDIA_ROOT` | `/app/mediafiles` |
19+
| `CORS_ALLOWED_ORIGINS` | `https://your-frontend.vercel.app` |
20+
| `CSRF_TRUSTED_ORIGINS` | `https://your-frontend.vercel.app,https://*.railway.app` |
21+
22+
Auto-set by Railway: `DATABASE_URL`
23+
24+
### Key Files
25+
26+
- `config/settings/railway.py` - Railway-specific settings (whitenoise, local cache, no AWS)
27+
- `requirements/railway.txt` - Dependencies without AWS packages
28+
- `start.sh` - Startup script (seeds media to volume, runs migrations, starts gunicorn)
29+
- `nixpacks.toml` - Points to `start.sh`
30+
- `Procfile` - Fallback start command
31+
32+
### Debug Endpoint
33+
34+
`/debug-media/` - Shows MEDIA_ROOT contents and file counts
35+
36+
---
37+
38+
## Frontend (Vercel)
39+
40+
### Setup
41+
42+
1. Import GitHub repo in Vercel
43+
2. Framework: **Next.js** (auto-detected)
44+
3. Set environment variables (see below)
45+
4. Deploy
46+
47+
### Environment Variables
48+
49+
| Variable | Value |
50+
|----------|-------|
51+
| `NEXT_PUBLIC_BACKEND_HOST` | `https://your-backend.up.railway.app` |
52+
| `NEXTAUTH_URL` | `https://your-frontend.vercel.app` |
53+
| `SECRET` | Any random string |
54+
55+
### Custom Domain
56+
57+
1. Vercel: **Settings****Domains** → Add `your.domain.com`
58+
2. DNS: Add CNAME record `your``cname.vercel-dns.com`
59+
3. Update `NEXTAUTH_URL` to match
60+
61+
---
62+
63+
## Architecture
64+
65+
```
66+
┌─────────────────┐ ┌──────────────────┐
67+
│ Vercel │────▶│ Railway │
68+
│ (Next.js) │ │ (Django) │
69+
└─────────────────┘ └────────┬─────────┘
70+
71+
┌────────────┼────────────┐
72+
▼ ▼ ▼
73+
PostgreSQL Volume Whitenoise
74+
(database) (media) (static)
75+
```
76+
77+
| Component | Solution |
78+
|-----------|----------|
79+
| Static files | Whitenoise (served from container) |
80+
| Media files | Railway Volume at `/app/mediafiles` |
81+
| Database | Railway PostgreSQL |
82+
| Cache | Local memory |
83+
| Email | Console backend (logs) |

nixpacks.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Use shell script for start command
2+
[start]
3+
cmd = "bash start.sh"

railway.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$schema": "https://railway.app/railway.schema.json",
3+
"build": {
4+
"builder": "NIXPACKS"
5+
},
6+
"deploy": {
7+
"restartPolicyType": "ON_FAILURE",
8+
"restartPolicyMaxRetries": 10
9+
}
10+
}

requirements/railway.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Railway deployment requirements
2+
# Simplified production without AWS dependencies
3+
4+
-r base.txt
5+
6+
# Web server
7+
gunicorn==22.0.0
8+
9+
# Database (Railway provides Postgres, or use SQLite)
10+
psycopg2-binary==2.9.9
11+
12+
# Static files
13+
whitenoise==6.7.0
14+
15+
# Note: The following are NOT included (vs production.txt):
16+
# - boto3 (S3)
17+
# - django-storages (S3)
18+
# - django-anymail (SES email)
19+
# - Collectfast (S3 optimization)

0 commit comments

Comments
 (0)