Commit Graph

178 Commits

Author SHA1 Message Date
40b4dd53a9 cache the subscription plans list
subscription/views.py
  - PlansListView: served from cache (subscription:plans:v1, 1h TTL);
    plans change ~never and the list is read on every subscription screen.
subscription/receivers.py (new)
  - post_save/post_delete on Plan -> cache.delete(PLANS_CACHE_KEY).
subscription/apps.py
  - SubscriptionConfig.ready() imports receivers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 08:46:30 +03:30
d25ecab0b3 optional short-TTL cache for the auth user row
TadrisinoTokenAuthentication ran one SELECT on accounts_user for every
authenticated request. Off by default (AUTH_USER_CACHE_TTL=0) so behaviour
is unchanged until a deployment opts in.

accounts/authentication.py
  - _get_request_user(): when AUTH_USER_CACHE_TTL>0, cache all concrete
    field values under auth:user:<id> and rebuild a full User instance
    (so save(update_fields=[...]) stays safe); otherwise load from DB as
    before. invalidate_user_cache() / _load_user_or_fail() helpers.
accounts/receivers.py (new)
  - post_save/post_delete on User -> invalidate_user_cache(pk); covers
    every in-app write path (SetUserGrade, login, refresh, last_activity).
accounts/apps.py
  - AccountsConfig.ready() imports receivers.
accounts/tests.py
  - AuthUserRowCacheTests: disabled -> loads every request; enabled ->
    loads once then cache; User.save() invalidates; SetUserGradeView still
    persists and the next request sees the new grade.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 08:46:11 +03:30
ff57adbc5f throttle hot read endpoints and cap unbounded list responses
progress/views.py
  - GuestAwareReadThrottle (same guest-by-IP pattern as the other apps);
    throttle_scope='read' on UserSummaryView, StreakCalendarView,
    LeaderboardView.
notifications/views.py
  - GuestAwareReadThrottle + throttle_scope='read' on NotificationListView.
  - list capped at NOTIFICATIONS_MAX=100 newest (Meta.ordering already
    newest-first); unreadCount still counted over the full set.
notifications/tests.py
  - 150 rows -> 100 returned newest-first, unreadCount = 75.
rag/views.py
  - ChunkListView: explicit order_by('-id') before the existing [:200] cap.
ai_tutor/views.py
  - ConversationMessagesView: return the newest 200 messages in
    chronological order instead of the whole conversation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 08:45:43 +03:30
35218bf6a1 fix HomeView N+1 and cache the curriculum tree
GET /api/home is the most-hit authenticated endpoint (every app open) and
was running one COUNT per node. Now it is a flat number of queries
regardless of grade size, and the shared curriculum skeleton is served
from cache; per-user progress/stats stay live.

content/views.py
  - _node_status(): takes a precomputed set of completed lesson_ids and a
    node's lesson_ids instead of firing its own COUNT query.
  - HomeView.get(): one query for all completed lesson_ids of the user in
    the grade, status computed in Python; grade tree (units -> nodes ->
    lesson_ids, no per-user data) read from cache
    (content:tree:grade:{id}:v{N}); missing grade -> 404 (was an
    unhandled 500); grade with no units -> empty units, not 404.
  - GradesListView: served from cache (content:grades:v{N}).
  - curriculum_version() / bump_curriculum_version(): one shared version
    key; bumping it invalidates every content cache key at once.
  - both GET views get throttle_scope='read' + GuestAwareContentThrottle.
content/receivers.py
  - post_save/post_delete on Grade/Unit/Node/Lesson -> bump_curriculum_version().
content/tests.py
  - HomeViewScalabilityTests: query count flat for 2 vs 20 nodes; 2nd call
    hits cache (no content_unit/node SQL); a new Node invalidates the tree;
    missing grade 404; empty grade 200.
  - GradesListCacheTests: list cached, invalidated on new Grade.
  - ReadThrottleTests: /api/home returns 429 past the limit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 08:45:18 +03:30
eeb6bd6138 scale prep — persistent DB connections, read throttle, prod renderer, auth-cache knob
settings.py
  - DATABASES.default.CONN_MAX_AGE from DB_CONN_MAX_AGE env (default 60);
    harmless on SQLite, saves a TCP+auth handshake per request on Postgres.
  - DEFAULT_RENDERER_CLASSES: BrowsableAPIRenderer only when DEBUG — no
    HTML API browser or its overhead in production.
  - DEFAULT_THROTTLE_RATES['read'] = READ_THROTTLE_RATE env (default
    120/min): a cheap ceiling for the hot GET endpoints.
  - AUTH_USER_CACHE_TTL env (default 0 = off): TTL for the per-request
    user-row cache added in a later commit; 0 keeps current behaviour.
.env.example
  - document DB_CONN_MAX_AGE / AUTH_USER_CACHE_TTL / READ_THROTTLE_RATE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 08:44:50 +03:30
40d358ecb5 readme: add RAG / AI-assistant section
README.md
  - `rag` app row in the app table.
  - new "دستیار هوش مصنوعی (RAG)" subsection: what /api/ai/ask does now,
    the OPENAI_/QDRANT_ keys it needs, how to test it in Swagger, the
    stream flag, rag_catalog, and the internal ingest endpoints.
  - production-notes bullet updated: /api/ai/ask is real RAG when keys are
    set, no longer "stub only".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 02:39:48 +03:30
149f1ad9ae return mascotMood on answer submission
The math-app frontend prototype drives a RobotMascot with a fixed set of
moods; this gives that animation an explicit backend signal. Complements
the existing Lesson.mascot_animation / LessonIntro.mascotAnimation, which
are unchanged.

serializers.py
  - AnswerResponseSerializer: new mascotMood field.
views.py
  - QuestionAnswerView: mascotMood = 'happy' for a correct answer else
    'encourage', on both the fresh response and the idempotent-replay
    response.
tests.py
  - QuestionAnswerMascotMoodTests: correct -> happy, wrong -> encourage,
    and the mood is preserved across an idempotent replay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 02:30:50 +03:30
5416fbb2ba real RAG answers on POST /api/ai/ask, plus SSE streaming and answer cache
When RAG is configured the endpoint retrieves lesson-scoped chunks and
generates a grounded answer; otherwise the previous deterministic stub
runs unchanged, so keyless runs and existing tests are unaffected.
Quota / idempotency / user-row lock / clean-503 behaviour is preserved on
every path.

models.py
  - Conversation.subject (CharField) and Conversation.lesson_number
    (IntegerField): retrieval metadata sent on the first ask and reused
    for the rest of the conversation.
migrations/0004_conversation_lesson_number_conversation_subject.py
  - migration for the two new fields.
serializers.py
  - AskRequestSerializer: optional subject / grade / lessonNumber to pin
    the Qdrant collection and lesson/chapter.
  - AskResponseSerializer: usedChunks (retrieved [{title, summary}]) and
    retrievalDebug (collection / filterKey / chapter-or-lesson / counts,
    emitted only under DEBUG).
views.py
  - generate_tutor_answer(): unchanged signature; RAG-configured branch
    calls rag.services generation, stub branch otherwise.
  - AskAIView.post(): resolve subject/grade/lessonNumber (falling back to
    the Conversation), check the answer cache via resolve_target(), run
    retrieve_context + generate_answer on a miss and populate the cache,
    return usedChunks (+ retrievalDebug under DEBUG). Failures still
    release the reserved quota slot and surface as 503.
  - AskAIView._streaming_response(): opt-in "stream": true returns a
    text/event-stream of chunk / done / error events; same retrieval,
    phase-3 persist, idempotency and quota accounting as the JSON path,
    and it consults the answer cache first. Non-stream JSON contract is
    untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 02:30:33 +03:30
ff47c8d4ba register rag app, RAG provider settings, autoload .env
settings.py
  - load .env via python-dotenv (override=False) so local runs and the RAG
    keys work without `export`; production/docker env still wins.
  - add 'rag' to INSTALLED_APPS.
  - new RAG / AI provider block: OPENAI_API_KEY / OPENAI_BASE_URL (GapGPT)
    / OPENAI_CHAT_MODEL / OPENAI_VISION_MODEL / OPENAI_EMBEDDING_MODEL /
    OPENAI_TIMEOUT_SECONDS, QDRANT_URL / QDRANT_API_KEY,
    RAG_COLLECTION_NAME_TEMPLATE / RAG_GRADE_TITLE_OVERRIDES /
    RAG_RETRIEVAL_TOP_K / RAG_FORCE_MOCK / RAG_ANSWER_CACHE_TTL.
  - add 'rag_ingest' to DEFAULT_THROTTLE_RATES.
  - under `manage.py test`: force RAG_FORCE_MOCK on and RAG_ANSWER_CACHE_TTL
    to 0 so no test hits OpenAI/Qdrant or a warm cache.
urls.py
  - mount rag.urls under /api/.
requirements.txt
  - python-dotenv; openai, qdrant-client; pymupdf / pdf2image / pillow
    (optional, only for PDF ingestion).
.env.example
  - document the OPENAI_* / QDRANT_* / RAG_* keys, the discovered Qdrant
    collection names, and the parked OPENAI_API_KEY_OPENAI_DIRECT slot
    (no provider-switch logic by design).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 02:30:12 +03:30
ac93e17305 new app — cleaned-up RAG pipeline (Qdrant retrieval + GPT), ported from Rag_backend
Self-contained app; wired into settings/urls in the next commit. Data
stays in the parallel team's live Qdrant — nothing is copied into our DB.

apps.py
  - RagConfig.
models.py
  - LessonChunk: DB-of-record for one summarized lesson chunk
    (original_text + summary + embedding_vector) mirrored as a single
    Qdrant point; loose lesson_id/grade_id ints, no cross-app FK.
    LessonChunkImage for future multimodal lessons.
migrations/0001_initial.py
  - initial tables for the two models.
services/clients.py
  - lazy, lru_cache'd OpenAI + Qdrant clients (Rag_backend builds them at
    import time and crashes without keys). RagNotConfigured raised when a
    key/lib is missing; Qdrant client uses check_compatibility=False for
    the 1.19-client / 1.17-server minor version gap.
services/config.py
  - rag_is_configured(), collection-name template, grade-title resolution
    (RAG_GRADE_TITLE_OVERRIDES -> content.Grade.title), retrieval top-k.
services/known_collections.py
  - exact live Qdrant catalog: 6 collection names, math chapter-number ->
    exact "فصل N: ..." string tables per (subject, grade), and the
    علوم/اجتماعی lesson-number -> title maps. filter_key_for_subject()
    picks `chapter` for math, `lesson_number` otherwise;
    resolve_math_chapter() maps a plain chapter number (Persian or Latin
    digits) to the stored string.
services/embedding.py
  - embed_text() pinned to OPENAI_EMBEDDING_MODEL (text-embedding-3-small,
    1536d) so vectors match the other team's existing collections.
services/chunking.py
  - split_text_into_chunks(), summarize_chunk() (gpt-4o-mini, JSON out);
    plain exceptions instead of ninja HttpError.
services/qdrant_store.py
  - ensure_collection(), upsert_points(), search(): the lesson filter
    matches the value both as str and native, since Qdrant stores
    lesson_number as int and chapter as string.
services/retrieval.py
  - resolve_target() -> (collection, filter_key, filter_value), reused by
    the cache key and debug block.
  - retrieve_context() -> (chunks, meta): embed -> Qdrant search scoped to
    one lesson/chapter -> top-k. Math payloads (no `summary`) fall back to
    generalSolution/problem/solution; a 404 collection returns empty.
services/generation.py
  - generate_answer() and stream_answer() (token iterator for SSE);
    context-grounded Persian prompt that explains from retrieved text
    rather than refusing.
services/pdf.py
  - textbook PDF -> page images -> gpt-4o vision problem-type extraction
    (adapted from Rag_backend mathapp); returns dicts, no on-disk JSON.
    pymupdf/pdf2image optional — missing libs raise RagNotConfigured.
services/ingest.py
  - ingest_text() / ingest_pdf(): chunk -> summarize -> embed -> persist
    LessonChunk + upsert Qdrant, in one transaction.
services/answer_cache.py
  - short-TTL cache of the final answer keyed by
    sha1(collection | filter_value | normalized-question).
services/tutor.py
  - thin answer_question() (retrieve -> generate) for direct callers.
serializers.py, urls.py, views.py
  - POST /api/rag/ingest/text, POST /api/rag/ingest/pdf (HasInternalServiceKey,
    throttle scope rag_ingest), GET /api/rag/chunks review list.
    RagProviderUnavailable -> clean 503.
admin.py
  - LessonChunk admin with an inline for its images.
management/commands/rag_catalog.py
  - read-only live Qdrant listing (--values, --json); never writes.
tests.py
  - mocked-client tests: ask-with-retrieval, ingest, SSE stream, answer
    cache, and rag_is_configured() gating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 02:29:54 +03:30
38999f3541 editor: add django-stubs/DRF-stubs for accurate type checking in VS Code 2026-08-26 04:04:36 +03:30
a33e575cfb readme: document docker exec usage for manage.py commands 2026-08-26 03:23:19 +03:30
683bd40169 cleanup: trim bloated multi-line comments across the project 2026-08-26 03:22:20 +03:30
83435c5a5f error-handling: add global exception handling, guard Bazaar API calls 2026-08-26 03:22:20 +03:30
49d596f077 readme: split into dev quick-start and production deploy guides 2026-08-26 03:22:20 +03:30
05ab6ffc36 admin & data: correct admin action redirect, add SQLite retry, seed_test_data command 2026-08-26 03:22:20 +03:30
f322d27e4f Merge remote-tracking branch 'origin/main' 2026-08-25 03:41:50 +03:30
a9200772a3 Add campaigns/ Add supervisor/ Add messaging features/ Separate SMS 2026-08-25 03:39:41 +03:30
7b9ab3adc3 Update README.md 2026-08-23 23:58:24 +03:30
80298b47b1 throttling, abuse-protection & logging/monitoring pass 2026-08-23 23:44:00 +03:30
cb18c93a3e Harden auth, AI tutor, payments, and subscriptions 2026-08-22 04:42:36 +03:30
f945f960c9 Update project 2026-08-19 17:15:55 +03:30
02d40e1238 Merge updated project structure with previous history 2026-08-18 16:54:53 +03:30
331148037e Initial commit - clean project structure 2026-08-18 16:48:15 +03:30
ab1560aba5 Update Swagger API documentation 2026-08-11 17:08:31 +03:30
60e656f6af Update README.md 2026-08-11 16:57:14 +03:30
b0587a42d8 Update README.md 2026-08-11 16:54:25 +03:30
e1d830f6a9 Initial commit 2026-08-11 16:43:12 +03:30