File size: 10,868 Bytes
15145f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
from bson import ObjectId
from database import get_db
from models.collections import GROUP_TESTS, GROUP_TEST_RESULTS, TOPICS, USERS, RESULTS
from utils.helpers import utc_now, str_objectid, str_objectids


# ─── helpers ─────────────────────────────────────────────────────────────────

async def _enrich_group_test(doc: dict, db) -> dict:
    """Attach topic names to a group test document."""
    enriched = str_objectid(doc)
    topics = []
    for tid in (doc.get("topic_ids") or []):
        try:
            t = await db[TOPICS].find_one({"_id": ObjectId(tid)})
        except Exception:
            t = None
        if t:
            topics.append({"id": str(t["_id"]), "name": t.get("name", "")})
    enriched["topics"] = topics
    return enriched


async def _refresh_topic_statuses(result: dict, db) -> dict:
    """Check RESULTS collection for each topic session and update statuses in-place."""
    topic_results = result.get("topic_results") or []
    updated = False
    for tr in topic_results:
        if tr.get("status") == "completed":
            continue
        session_id = tr.get("session_id")
        if not session_id:
            continue
        report = await db[RESULTS].find_one({"session_id": session_id})
        if report:
            tr["status"] = "completed"
            tr["overall_score"] = report.get("overall_score", 0)
            tr["total_questions"] = report.get("total_questions", 0)
            tr["completed_at"] = report.get("completed_at")
            updated = True

    if updated:
        result_id = str(result.get("_id") or result.get("id", ""))
        all_done = all(tr.get("status") == "completed" for tr in topic_results)
        if all_done:
            scores = [tr.get("overall_score") or 0 for tr in topic_results]
            overall = round(sum(scores) / len(scores), 1) if scores else 0.0
            await db[GROUP_TEST_RESULTS].update_one(
                {"_id": ObjectId(result_id)},
                {
                    "$set": {
                        "topic_results": topic_results,
                        "status": "completed",
                        "overall_score": overall,
                        "completed_at": utc_now(),
                    }
                },
            )
            result["status"] = "completed"
            result["overall_score"] = overall
        else:
            await db[GROUP_TEST_RESULTS].update_one(
                {"_id": ObjectId(result_id)},
                {"$set": {"topic_results": topic_results}},
            )
        result["topic_results"] = topic_results
    return result


# ─── Admin CRUD ───────────────────────────────────────────────────────────────

async def create_group_test(
    name: str,
    description: str | None,
    topic_ids: list[str],
    time_limit_minutes: int | None,
    max_attempts: int,
    created_by: str,
) -> dict:
    db = get_db()

    if not topic_ids:
        raise ValueError("At least one topic is required")

    # Validate every topic exists
    for tid in topic_ids:
        try:
            t = await db[TOPICS].find_one({"_id": ObjectId(tid)})
        except Exception:
            raise ValueError(f"Invalid topic ID: {tid}")
        if not t:
            raise ValueError(
                f"Topic with ID '{tid}' does not exist. Create it in Topics before adding here."
            )

    doc = {
        "name": name.strip(),
        "description": (description or "").strip() or None,
        "topic_ids": topic_ids,
        "time_limit_minutes": time_limit_minutes,
        "max_attempts": max(1, int(max_attempts)),
        "is_published": False,
        "created_by": created_by,
        "created_at": utc_now(),
    }
    result = await db[GROUP_TESTS].insert_one(doc)
    doc["_id"] = result.inserted_id
    return await _enrich_group_test(doc, db)


async def list_group_tests(only_published: bool = False) -> list:
    db = get_db()
    query = {"is_published": True} if only_published else {}
    cursor = db[GROUP_TESTS].find(query).sort("created_at", -1)
    docs = await cursor.to_list(length=200)
    return [await _enrich_group_test(d, db) for d in docs]


async def get_group_test(group_test_id: str) -> dict:
    db = get_db()
    doc = await db[GROUP_TESTS].find_one({"_id": ObjectId(group_test_id)})
    if not doc:
        raise ValueError("Group test not found")
    return await _enrich_group_test(doc, db)


async def update_group_test(group_test_id: str, data: dict) -> dict:
    db = get_db()

    # Validate topic IDs if provided
    if "topic_ids" in data and data["topic_ids"] is not None:
        if not data["topic_ids"]:
            raise ValueError("At least one topic is required")
        for tid in data["topic_ids"]:
            try:
                t = await db[TOPICS].find_one({"_id": ObjectId(tid)})
            except Exception:
                raise ValueError(f"Invalid topic ID: {tid}")
            if not t:
                raise ValueError(
                    f"Topic with ID '{tid}' does not exist. Create it in Topics before adding here."
                )

    update_data = {k: v for k, v in data.items() if v is not None}
    if "max_attempts" in update_data:
        update_data["max_attempts"] = max(1, int(update_data["max_attempts"]))
    update_data["updated_at"] = utc_now()

    await db[GROUP_TESTS].update_one(
        {"_id": ObjectId(group_test_id)}, {"$set": update_data}
    )
    return await get_group_test(group_test_id)


async def delete_group_test(group_test_id: str) -> bool:
    db = get_db()
    result = await db[GROUP_TESTS].delete_one({"_id": ObjectId(group_test_id)})
    return result.deleted_count > 0


async def set_group_test_publish(group_test_id: str, is_published: bool) -> dict:
    db = get_db()
    await db[GROUP_TESTS].update_one(
        {"_id": ObjectId(group_test_id)},
        {"$set": {"is_published": is_published, "updated_at": utc_now()}},
    )
    return await get_group_test(group_test_id)


async def get_group_test_results_admin(group_test_id: str) -> list:
    """All student results for a given group test (admin view)."""
    db = get_db()
    cursor = db[GROUP_TEST_RESULTS].find({"group_test_id": group_test_id}).sort(
        "started_at", -1
    )
    docs = await cursor.to_list(length=500)
    return str_objectids(docs)


# ─── Student ──────────────────────────────────────────────────────────────────

async def start_group_test_attempt(group_test_id: str, user_id: str) -> dict:
    db = get_db()

    group_test = await db[GROUP_TESTS].find_one({"_id": ObjectId(group_test_id)})
    if not group_test:
        raise ValueError("Group test not found")
    if not group_test.get("is_published"):
        raise ValueError("This group test is not available")

    max_attempts = int(group_test.get("max_attempts") or 1)
    attempt_count = await db[GROUP_TEST_RESULTS].count_documents(
        {"group_test_id": group_test_id, "user_id": user_id}
    )
    if attempt_count >= max_attempts:
        raise ValueError(
            f"You have reached the maximum number of attempts ({max_attempts}) for this group test."
        )

    user = await db[USERS].find_one({"_id": ObjectId(user_id)})

    topic_results = []
    for tid in (group_test.get("topic_ids") or []):
        t = await db[TOPICS].find_one({"_id": ObjectId(tid)})
        topic_results.append(
            {
                "topic_id": tid,
                "topic_name": t.get("name", "") if t else "",
                "session_id": None,
                "status": "pending",
                "overall_score": None,
                "total_questions": None,
                "completed_at": None,
            }
        )

    doc = {
        "group_test_id": group_test_id,
        "group_test_name": group_test.get("name", ""),
        "user_id": user_id,
        "user_name": (user or {}).get("name", ""),
        "user_email": (user or {}).get("email", ""),
        "attempt_number": attempt_count + 1,
        "topic_results": topic_results,
        "overall_score": None,
        "status": "in_progress",
        "started_at": utc_now(),
        "completed_at": None,
        "time_limit_minutes": group_test.get("time_limit_minutes"),
    }
    result = await db[GROUP_TEST_RESULTS].insert_one(doc)
    doc["_id"] = result.inserted_id
    return str_objectid(doc)


async def get_group_test_result(result_id: str, user_id: str) -> dict:
    db = get_db()
    result = await db[GROUP_TEST_RESULTS].find_one({"_id": ObjectId(result_id)})
    if not result:
        raise ValueError("Group test result not found")
    if result.get("user_id") != user_id:
        raise ValueError("Unauthorized")
    result = await _refresh_topic_statuses(result, db)
    return str_objectid(result)


async def link_topic_session(
    result_id: str, user_id: str, topic_id: str, session_id: str
) -> dict:
    """Store session_id for a topic so its completion can be tracked."""
    db = get_db()
    result = await db[GROUP_TEST_RESULTS].find_one({"_id": ObjectId(result_id)})
    if not result:
        raise ValueError("Group test result not found")
    if result.get("user_id") != user_id:
        raise ValueError("Unauthorized")

    topic_results = result.get("topic_results") or []
    found = False
    for tr in topic_results:
        if tr.get("topic_id") == topic_id:
            tr["session_id"] = session_id
            if tr.get("status") == "pending":
                tr["status"] = "in_progress"
            found = True
            break

    if not found:
        raise ValueError("Topic not found in this group test")

    await db[GROUP_TEST_RESULTS].update_one(
        {"_id": ObjectId(result_id)},
        {"$set": {"topic_results": topic_results}},
    )
    return await get_group_test_result(result_id, user_id)


async def get_my_group_test_results(user_id: str) -> list:
    db = get_db()
    cursor = db[GROUP_TEST_RESULTS].find({"user_id": user_id}).sort("started_at", -1)
    docs = await cursor.to_list(length=100)
    return str_objectids(docs)


async def get_my_group_test_attempt(group_test_id: str, user_id: str) -> dict | None:
    """Return latest attempt result for a group test, or None."""
    db = get_db()
    doc = await db[GROUP_TEST_RESULTS].find_one(
        {"group_test_id": group_test_id, "user_id": user_id},
        sort=[("attempt_number", -1)],
    )
    if not doc:
        return None
    doc = await _refresh_topic_statuses(doc, db)
    return str_objectid(doc)