github-actions[bot] commited on
Commit
2f5bcd1
·
1 Parent(s): 5cef14e

Auto-deploy backend from GitHub Actions

Browse files
app/api/v1/api_router.py CHANGED
@@ -1,16 +1,16 @@
1
  from fastapi import APIRouter
2
- from app.api.v1.endpoints import discover, chat, pathfinder, learn
3
 
4
  api_router = APIRouter()
5
 
6
- api_router.include_router(
7
- discover.router, prefix="/discover", tags=["Discovery"])
8
 
9
- api_router.include_router(
10
- chat.router, prefix="/chat", tags=["Tutor API"])
11
 
12
  api_router.include_router(
13
- pathfinder.router, prefix="/pathfinder", tags=["Pathfinder Engine"])
 
14
 
15
- api_router.include_router(
16
- learn.router, prefix="/learn", tags=["Learning Cards"])
 
 
1
  from fastapi import APIRouter
2
+ from app.api.v1.endpoints import discover, learn, pathfinder, chat, pathways
3
 
4
  api_router = APIRouter()
5
 
6
+ api_router.include_router(discover.router, prefix="/discover", tags=["Discovery"])
 
7
 
8
+ api_router.include_router(chat.router, prefix="/chat", tags=["Tutor API"])
 
9
 
10
  api_router.include_router(
11
+ pathfinder.router, prefix="/pathfinder", tags=["Pathfinder Engine"]
12
+ )
13
 
14
+ api_router.include_router(learn.router, prefix="/learn", tags=["Learning Cards"])
15
+
16
+ api_router.include_router(pathways.router, prefix="/pathways", tags=["pathways"])
app/api/v1/endpoints/pathways.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from fastapi import APIRouter, Depends, HTTPException, status
3
+ from supabase import Client
4
+ from app.core.security import get_user_db_client
5
+ from app.schemas.pathways import (
6
+ PathwayCatalogResponse,
7
+ PathwaySchema,
8
+ PathwayTaskSchema,
9
+ )
10
+
11
+ logger = logging.getLogger(__name__)
12
+ router = APIRouter()
13
+
14
+
15
+ @router.get("/catalog", response_model=PathwayCatalogResponse)
16
+ async def get_pathway_catalog(
17
+ db_data: tuple[Client, str] = Depends(get_user_db_client),
18
+ ):
19
+ db_client, user_id = db_data
20
+ try:
21
+ # 1. Fetch all pathways and their underlying tasks in a single query
22
+ catalog_res = (
23
+ db_client.table("pathways")
24
+ .select("*, pathway_tasks(id, task_description, order_index)")
25
+ .execute()
26
+ )
27
+
28
+ # 2. Fetch the user's specific progress state
29
+ user_state_res = (
30
+ db_client.table("user_pathways")
31
+ .select("pathway_id, status, user_pathway_tasks(task_id, is_completed)")
32
+ .eq("user_id", user_id)
33
+ .execute()
34
+ )
35
+
36
+ # 3. Map user state for O(1) memory lookups
37
+ user_enrollments = {
38
+ row["pathway_id"]: {
39
+ "status": row["status"],
40
+ "tasks": {
41
+ t["task_id"]: t["is_completed"]
42
+ for t in row.get("user_pathway_tasks", [])
43
+ },
44
+ }
45
+ for row in user_state_res.data
46
+ }
47
+
48
+ pathways_list = []
49
+ total_points_earned = 0
50
+ active_count = 0
51
+ total_progress_sum = 0
52
+
53
+ for p in catalog_res.data:
54
+ p_id = p["id"]
55
+ u_state = user_enrollments.get(p_id, {"status": "available", "tasks": {}})
56
+
57
+ status_val = u_state["status"]
58
+
59
+ # Sort tasks safely by order_index
60
+ raw_tasks = sorted(
61
+ p.get("pathway_tasks", []), key=lambda x: x.get("order_index", 0)
62
+ )
63
+
64
+ mapped_tasks = []
65
+ completed_tasks = 0
66
+
67
+ for rt in raw_tasks:
68
+ is_done = u_state["tasks"].get(rt["id"], False)
69
+ if is_done:
70
+ completed_tasks += 1
71
+ mapped_tasks.append(
72
+ PathwayTaskSchema(
73
+ id=rt["id"],
74
+ description=rt["task_description"],
75
+ is_completed=is_done,
76
+ )
77
+ )
78
+
79
+ # Calculate dynamic progress
80
+ task_count = len(mapped_tasks)
81
+ progress_pct = (
82
+ int((completed_tasks / task_count * 100)) if task_count > 0 else 0
83
+ )
84
+
85
+ if status_val == "active":
86
+ active_count += 1
87
+ total_progress_sum += progress_pct
88
+ elif status_val == "completed":
89
+ total_points_earned += p.get("total_points", 0)
90
+
91
+ pathways_list.append(
92
+ PathwaySchema(
93
+ id=p_id,
94
+ title=p["title"],
95
+ description=p["description"],
96
+ image_url=p["image_url"],
97
+ difficulty=p["difficulty"],
98
+ total_points=p.get("total_points", 0),
99
+ target_strand=p.get("target_strand", "GENERAL"),
100
+ status=status_val,
101
+ progress_percentage=progress_pct,
102
+ tasks=mapped_tasks,
103
+ )
104
+ )
105
+
106
+ average_progress = (
107
+ (total_progress_sum / active_count) if active_count > 0 else 0.0
108
+ )
109
+
110
+ return PathwayCatalogResponse(
111
+ active_pathways_count=active_count,
112
+ average_progress=round(average_progress, 1),
113
+ total_points_earned=total_points_earned,
114
+ pathways=pathways_list,
115
+ )
116
+
117
+ except Exception as e:
118
+ logger.error(f"Catalog fetch failed for user {user_id}: {e}", exc_info=True)
119
+ raise HTTPException(status_code=500, detail="Failed to load pathways catalog")
120
+
121
+
122
+ @router.post("/{pathway_id}/enroll")
123
+ async def enroll_in_pathway(
124
+ pathway_id: str, db_data: tuple[Client, str] = Depends(get_user_db_client)
125
+ ):
126
+ db_client, user_id = db_data
127
+ try:
128
+ # 1. Ensure pathway exists and user isn't already enrolled
129
+ existing = (
130
+ db_client.table("user_pathways")
131
+ .select("id")
132
+ .eq("user_id", user_id)
133
+ .eq("pathway_id", pathway_id)
134
+ .execute()
135
+ )
136
+ if existing.data:
137
+ raise HTTPException(
138
+ status_code=400,
139
+ detail="User is already enrolled or has abandoned this pathway.",
140
+ )
141
+
142
+ # 2. Create Enrollment
143
+ enroll_res = (
144
+ db_client.table("user_pathways")
145
+ .insert({"user_id": user_id, "pathway_id": pathway_id, "status": "active"})
146
+ .execute()
147
+ )
148
+
149
+ user_pathway_id = enroll_res.data[0]["id"]
150
+
151
+ # 3. Fetch all tasks for this pathway
152
+ tasks_res = (
153
+ db_client.table("pathway_tasks")
154
+ .select("id")
155
+ .eq("pathway_id", pathway_id)
156
+ .execute()
157
+ )
158
+
159
+ # 4. Create empty progress rows for each task so the tracker works immediately
160
+ task_inserts = [
161
+ {"user_id": user_id, "task_id": t["id"], "user_pathway_id": user_pathway_id}
162
+ for t in tasks_res.data
163
+ ]
164
+
165
+ if task_inserts:
166
+ db_client.table("user_pathway_tasks").insert(task_inserts).execute()
167
+
168
+ return {"status": "success", "message": "Successfully enrolled in Pathway!"}
169
+ except HTTPException:
170
+ raise
171
+ except Exception as e:
172
+ logger.error(f"Failed to enroll in pathway {pathway_id}: {e}", exc_info=True)
173
+ raise HTTPException(status_code=500, detail="Enrollment failed.")
app/schemas/pathways.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class PathwayTaskSchema(BaseModel):
5
+ id: str
6
+ description: str = Field(description="The visible task instruction for the user")
7
+ is_completed: bool = Field(default=False)
8
+
9
+
10
+ class PathwaySchema(BaseModel):
11
+ id: str
12
+ title: str
13
+ description: str
14
+ image_url: str
15
+ difficulty: str
16
+ total_points: int
17
+ target_strand: str
18
+ status: str = Field(description="'available', 'active', or 'completed'")
19
+ progress_percentage: int = Field(default=0)
20
+ tasks: list[PathwayTaskSchema] = []
21
+
22
+
23
+ class PathwayCatalogResponse(BaseModel):
24
+ active_pathways_count: int
25
+ average_progress: float
26
+ total_points_earned: int
27
+ pathways: list[PathwaySchema]