Files
MathApp/CLAUDE.md
Zareian f3be99e4e2 record where this project is heading, not just how it works
هر ایجنتِ تازه ساختارِ کد را می‌بیند ولی نمی‌داند صاحبِ پروژه دنبالِ چیست:
دو ریپو و یک قراردادِ API (نه ادغامِ ریپوها)، حذفِ هر قدمِ دستی از انتشار،
محتوای قابلِ ویرایش بدونِ برنامه‌نویس، و سنجش‌پذیر بودنِ کیفیتِ پاسخِ AI.

بدونِ این، پیشنهادها به سمتی می‌روند که با مسیرِ پروژه نمی‌خواند.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 02:44:03 +03:30

11 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project

Django + DRF backend for Tadrisino, an educational app (Persian/Farsi). Codebase and API responses are in Persian; comments in source are Persian too.

Commands

source venv/bin/activate
python manage.py runserver
python manage.py migrate
python manage.py test                 # all tests
python manage.py test accounts        # single app
python manage.py test accounts.tests.SomeTestCase.test_something  # single test

Local dev requires DJANGO_DEBUG=1 (enables Swagger auth via DEV_MASTER_OTP/devOtp in responses). DEBUG defaults to False. Production also requires DJANGO_SECRET_KEY, DJANGO_ALLOWED_HOSTS (comma-separated), and CACHE_URL (Redis — required whenever DEBUG=False since rate limiting needs a shared cache; raises ImproperlyConfigured otherwise).

No pytest; uses Django's built-in test runner. No linter/formatter configured.

Docker (docker compose up --build) adds a local Loki+Grafana Alloy+Grafana log stack; not a production setup (still runserver, not gunicorn). See comments in docker-compose.yml, docker/alloy/config.alloy, and LOG_DIR in config/settings.py.

API docs: Swagger /api/docs/, Redoc /api/redoc/, admin /admin/.

Architecture

Apps (each owns its own models, one responsibility):

app responsibility key models
accounts identity, sessions, OTP auth User, Profile, Session, OTPCode
content lessons/curriculum Grade, Unit, Node, Lesson, Question, Choice, NodeProgress
progress stats, leaderboard, streaks, gamification characters UserStats, LeaderboardEntry, StreakRecord, UserHearts, Character
ai_tutor AI assistant chat Conversation, Message, MessageFeedback
subscription plans Plan, Subscription
payment orders/transactions (Zarinpal gateway + Cafe Bazaar IAP) Order, Transaction
discount discount codes/offers DiscountCode, Redemption, Offer, UserOffer
notifications notifications Notification
affiliate referral program AffiliateAccount, Conversion
support tickets/bug reports BugReport, Ticket, TicketMessage
family parental control FamilyLink, ParentalSettings, WeeklyReportLog
statuslog generic cross-app status-change log StatusChangeLog
campaigns messaging campaign storage only, no send logic yet MessageCampaign
config project settings, root urls, middleware —

Auth is custom, not Django's. accounts.User is deliberately not AUTH_USER_MODEL — it's an independent business model. Auth is a stateless signed bearer token (django.core.signing, no JWT library) issued/verified in accounts/authentication.py (TadrisinoTokenAuthentication, header Authorization: Bearer <token>), backed by accounts.Session. Missing/invalid header → anonymous request, not a hard failure; each view's permission class decides if auth is required.

Apps are decoupled by convention: instead of real FKs across app boundaries, code uses loose refs (user_id, object_id + label) so any app can be split out later. Don't add cross-app FKs — follow the existing loose-reference pattern.

Throttling: DRF ScopedRateThrottle with per-endpoint throttle_scope strings, rates centralized in REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'] in config/settings.py. Guest users are throttled by IP (NUM_PROXIES setting controls how many XFF hops are trusted — must match real proxy count in prod or IP spoofing/throttle-bypass is possible). Adding a new sensitive endpoint means adding both a throttle_scope on the view and a matching rate entry in settings.

POST /api/ai/ask currently returns a stub; real model integration point is generate_tutor_answer in ai_tutor/views.py.

Structured logging: config.middleware.APIRequestLoggingMiddleware (last in MIDDLEWARE, closest to the view) logs every /api/ request as JSON (method/path/status/duration/user, no body/headers/secrets) via config/log_formatters.py.

SQLite is current DB (OPTIONS.timeout=20 to tolerate concurrent-write locks, e.g. campaigns tests); README notes Postgres migration is still pending.

SMS goes through Kavenegar (KAVENEGAR_API_KEY/KAVENEGAR_SENDER); failures return explicit 503, not a fake success.

Gamification characters (progress.Character, Duolingo-mascot-style): an admin-managed catalog (like subscription.Plan — created via Django admin or seed_test_data, not by users). Unlocking is currently just UserStats.xp >= Character.unlock_xp (no separate ownership table); UserStats.selected_character_id is the user's active pick, set via POST /api/characters/select after that check. First real content beyond the model/plumbing (art assets, more characters, purchase-with-coins, etc.) is expected to land incrementally later.

Payment (Zarinpal): create_order/CreateOrderView never call Zarinpal directly — paymentUrl points at our own GET /api/payment/orders/{id}/pay (PaymentRedirectView), which calls Zarinpal's PaymentRequest API on first visit (storing Order.gateway_authority) and redirects to StartPay; this keeps order creation from depending on Zarinpal's uptime. Zarinpal redirects the user back to GET /api/payment/zarinpal/callback (ZarinpalCallbackView), which always re-verifies server-to-server via payment/zarinpal.py::verify_payment before marking an order paid — a forged Status=OK in the query string alone does nothing. Both payment/zarinpal.py and payment/bazaar.py fail closed (503 / invalid) when their credentials aren't configured, never a fake success.

Where this is heading (the owner's intent)

The owner writes this backend; another developer writes the frontend; a third person administers the server. None of them share a machine, and until recently the two halves were built in parallel without a shared contract — which is why reconciling them cost rework. The goal is not to merge the repos. Two repos, one API contract is the intended shape, and the contract lives in this repo's OpenAPI schema (/api/docs/), not in anyone's head.

What the owner is steering toward, in rough order:

  1. Both halves running against each other, on real content, with nobody hand-editing files on the server. Most of the plumbing for this now exists.
  2. Nothing manual in a release. Adding a dependency, a setting or a service must not add a step someone has to remember. When a change would, fold it into deploy.sh or make startup fail loudly and legibly instead.
  3. Content editable without a developer — questions, curriculum, plan copy, all from the admin panel. A management command run over SSH is a last resort, not a workflow.
  4. Answer quality measurable, not assumed: per-question reports, wrong-answer rates, prompt-version feedback. The senior's stated priority is proving the AI actually teaches, especially where students get stuck.
  5. Scale without drama — thousands of concurrent users should be a non-event. Prefer the boring fix (an index, a cache, a cheap guard) over new infrastructure.

The owner is early in their career and learning the collaboration workflow as they go. Explain the why alongside the what, flag when something is a product decision rather than a coding one, and never assume a step was done just because it was described.

Frontend ↔ backend (read before wiring the two together)

The app is two repos and two domains: the React frontend lives in Tadrisino/MathApp_frontend (deployed at math.tadrisino2.ir, owned by another developer) and this backend serves api.math.tadrisino2.ir. Our typed client for them is src/api/ inside their repo, delivered by pull request — we have read-only Gitea access there, so changes go via a fork + PR, never a direct push.

The one trap that keeps biting. Their App.tsx calls relative paths (fetch('/api/auth/login')). Those resolve against the frontend origin, where nginx answers any unknown path with index.html — so the response is 200 with an HTML body and the code dies on res.json() with Unexpected token '<'. Verify with curl https://math.tadrisino2.ir/api/grades: HTTP 200, HTML. Two fixes exist, both shipped in PR #3: a vite dev proxy (dev works unchanged) and an exported apiUrl() for builds. The better long-term fix is infrastructure, not code — proxy /api on the frontend domain to this backend, which makes everything same-origin and removes CORS entirely. That needs the server admin, not us.

Order matters when bringing the stack up: backend first, then frontend. If the frontend is tested while api.math.tadrisino2.ir is down, every screen fails and it looks like the client is broken.

Never hand-write the client from memory. Three of fifteen calls were silently wrong the first time. Dump the real contract with python manage.py spectacular --file schema.yaml --format openapi, then drive the actual client module against a running server (runserver on a spare port + npx tsx) before sending anything. content/tests_frontend_contract.py is the backend-side guard.

Content the frontend expects but nobody has filled in yet: Unit.blurb, Plan.features/badge, Lesson.example/steps. All optional, all editable from the admin panel. Node.icon/color fall back to a deterministic palette so cards are never colourless.

Operations

./deploy.sh is the whole release: pull, install, migrate, collectstatic, restart, then it polls /api/health and prints the likely cause if the service does not come back. Settings boot with working defaults (hosts, CSRF, CORS, secret key, cache), so a stale or partial .env on the server no longer breaks startup — env values are merged with those defaults, not replaced by them.

GET /api/health is the outside view: database, unapplied migrations, cache, content counts, security warnings, and recent traffic. It is how you check whether anyone has actually connected without asking a human.

Known gaps (per README pre-deploy checklist)

  • Still on SQLite, not Postgres.
  • Throttle rates need periodic review against real traffic.
  • DEV_MASTER_OTP (0000) must stay gated behind DEBUG — never enabled in prod.
  • The server runs with DJANGO_DEBUG=1 on a public domain, so OTP 0000 currently logs anyone in as anyone. Harmless until real users exist; /api/health warns about it.
  • No video content exists yet, though Lesson.kind/video_url can express it.
  • Question generation and textbook ingest are blocked on AI API credit.