|
7 | 7 |
|
8 | 8 | import logging |
9 | 9 |
|
| 10 | +from dataclasses import dataclass |
| 11 | +from typing import Optional, Tuple |
10 | 12 | import edx_api_doc_tools as apidocs |
| 13 | +from edx_when import api as edx_when_api |
11 | 14 | from opaque_keys import InvalidKeyError |
12 | 15 | from opaque_keys.edx.keys import CourseKey, UsageKey |
13 | 16 | from rest_framework import status |
| 17 | +from rest_framework.generics import ListAPIView |
14 | 18 | from rest_framework.permissions import IsAuthenticated |
15 | 19 | from rest_framework.response import Response |
16 | 20 | from rest_framework.views import APIView |
|
34 | 38 | InstructorTaskListSerializer, |
35 | 39 | CourseInformationSerializerV2, |
36 | 40 | BlockDueDateSerializerV2, |
| 41 | + UnitExtensionSerializer, |
37 | 42 | ORASerializer, |
38 | 43 | ORASummarySerializer, |
39 | 44 | ) |
@@ -356,6 +361,164 @@ def get(self, request, course_id): |
356 | 361 | return Response(formated_subsections, status=status.HTTP_200_OK) |
357 | 362 |
|
358 | 363 |
|
| 364 | +@dataclass(frozen=True) |
| 365 | +class UnitDueDateExtension: |
| 366 | + """Dataclass representing a unit due date extension for a student.""" |
| 367 | + |
| 368 | + username: str |
| 369 | + full_name: str |
| 370 | + email: str |
| 371 | + unit_title: str |
| 372 | + unit_location: str |
| 373 | + extended_due_date: Optional[str] |
| 374 | + |
| 375 | + @classmethod |
| 376 | + def from_block_tuple(cls, row: Tuple, unit): |
| 377 | + username, full_name, due_date, email, location = row |
| 378 | + unit_title = title_or_url(unit) |
| 379 | + return cls( |
| 380 | + username=username, |
| 381 | + full_name=full_name, |
| 382 | + email=email, |
| 383 | + unit_title=unit_title, |
| 384 | + unit_location=location, |
| 385 | + extended_due_date=due_date, |
| 386 | + ) |
| 387 | + |
| 388 | + @classmethod |
| 389 | + def from_course_tuple(cls, row: Tuple, units_dict: dict): |
| 390 | + username, full_name, email, location, due_date = row |
| 391 | + unit_title = title_or_url(units_dict[str(location)]) |
| 392 | + return cls( |
| 393 | + username=username, |
| 394 | + full_name=full_name, |
| 395 | + email=email, |
| 396 | + unit_title=unit_title, |
| 397 | + unit_location=location, |
| 398 | + extended_due_date=due_date, |
| 399 | + ) |
| 400 | + |
| 401 | + |
| 402 | +class UnitExtensionsView(ListAPIView): |
| 403 | + """ |
| 404 | + Retrieve a paginated list of due date extensions for units in a course. |
| 405 | +
|
| 406 | + **Example Requests** |
| 407 | +
|
| 408 | + GET /api/instructor/v2/courses/{course_id}/unit_extensions |
| 409 | + GET /api/instructor/v2/courses/{course_id}/unit_extensions?page=2 |
| 410 | + GET /api/instructor/v2/courses/{course_id}/unit_extensions?page_size=50 |
| 411 | + GET /api/instructor/v2/courses/{course_id}/unit_extensions?email_or_username=john |
| 412 | + GET /api/instructor/v2/courses/{course_id}/unit_extensions?block_id=block-v1:org@problem+block@unit1 |
| 413 | +
|
| 414 | + **Response Values** |
| 415 | +
|
| 416 | + { |
| 417 | + "count": 150, |
| 418 | + "next": "http://example.com/api/instructor/v2/courses/course-v1:org+course+run/unit_extensions?page=2", |
| 419 | + "previous": null, |
| 420 | + "results": [ |
| 421 | + { |
| 422 | + "username": "student1", |
| 423 | + "full_name": "John Doe", |
| 424 | + "email": "john.doe@example.com", |
| 425 | + "unit_title": "Unit 1: Introduction", |
| 426 | + "unit_location": "block-v1:org+course+run+type@problem+block@unit1", |
| 427 | + "extended_due_date": "2023-12-25T23:59:59Z" |
| 428 | + }, |
| 429 | + ... |
| 430 | + ] |
| 431 | + } |
| 432 | +
|
| 433 | + **Parameters** |
| 434 | +
|
| 435 | + course_id: Course key for the course. |
| 436 | + page (optional): Page number for pagination. |
| 437 | + page_size (optional): Number of results per page. |
| 438 | +
|
| 439 | + **Returns** |
| 440 | +
|
| 441 | + * 200: OK - Returns paginated list of unit extensions |
| 442 | + * 401: Unauthorized - User is not authenticated |
| 443 | + * 403: Forbidden - User lacks instructor permissions |
| 444 | + * 404: Not Found - Course does not exist |
| 445 | + """ |
| 446 | + permission_classes = (IsAuthenticated, permissions.InstructorPermission) |
| 447 | + permission_name = permissions.VIEW_DASHBOARD |
| 448 | + serializer_class = UnitExtensionSerializer |
| 449 | + filter_backends = [] |
| 450 | + |
| 451 | + def _matches_email_or_username(self, unit_extension, filter_value): |
| 452 | + """ |
| 453 | + Check if the unit extension matches the email or username filter. |
| 454 | + """ |
| 455 | + return ( |
| 456 | + filter_value in unit_extension.username.lower() |
| 457 | + or filter_value in unit_extension.email.lower() |
| 458 | + ) |
| 459 | + |
| 460 | + def get_queryset(self): |
| 461 | + """ |
| 462 | + Returns the queryset of unit extensions for the specified course. |
| 463 | +
|
| 464 | + This method uses the core logic from get_overrides_for_course to retrieve |
| 465 | + due date extension data and transforms it into a list of normalized objects |
| 466 | + that can be paginated and serialized. |
| 467 | +
|
| 468 | + Supports filtering by: |
| 469 | + - email_or_username: Filter by username or email address |
| 470 | + - block_id: Filter by specific unit/subsection location |
| 471 | + """ |
| 472 | + course_id = self.kwargs["course_id"] |
| 473 | + course_key = CourseKey.from_string(course_id) |
| 474 | + course = get_course_by_id(course_key) |
| 475 | + |
| 476 | + email_or_username_filter = self.request.query_params.get("email_or_username") |
| 477 | + block_id_filter = self.request.query_params.get("block_id") |
| 478 | + |
| 479 | + units = get_units_with_due_date(course) |
| 480 | + units_dict = {str(u.location): u for u in units} |
| 481 | + |
| 482 | + # Fetch and normalize overrides |
| 483 | + if block_id_filter: |
| 484 | + try: |
| 485 | + unit = find_unit(course, block_id_filter) |
| 486 | + query_data = edx_when_api.get_overrides_for_block(course.id, unit.location) |
| 487 | + unit_due_date_extensions = [ |
| 488 | + UnitDueDateExtension.from_block_tuple(row, unit) |
| 489 | + for row in query_data |
| 490 | + ] |
| 491 | + except InvalidKeyError: |
| 492 | + # If block_id is invalid, return empty list |
| 493 | + unit_due_date_extensions = [] |
| 494 | + else: |
| 495 | + query_data = edx_when_api.get_overrides_for_course(course.id) |
| 496 | + unit_due_date_extensions = [ |
| 497 | + UnitDueDateExtension.from_course_tuple(row, units_dict) |
| 498 | + for row in query_data |
| 499 | + if str(row[3]) in units_dict # Ensure unit has due date |
| 500 | + ] |
| 501 | + |
| 502 | + # Apply filters if any |
| 503 | + filter_value = email_or_username_filter.lower() if email_or_username_filter else None |
| 504 | + |
| 505 | + results = [ |
| 506 | + extension |
| 507 | + for extension in unit_due_date_extensions |
| 508 | + if self._matches_email_or_username(extension, filter_value) |
| 509 | + ] if filter_value else unit_due_date_extensions # if no filter, use all |
| 510 | + |
| 511 | + # Sort for consistent ordering |
| 512 | + results.sort( |
| 513 | + key=lambda o: ( |
| 514 | + o.username, |
| 515 | + o.unit_title, |
| 516 | + ) |
| 517 | + ) |
| 518 | + |
| 519 | + return results |
| 520 | + |
| 521 | + |
359 | 522 | class ORAView(GenericAPIView): |
360 | 523 | """ |
361 | 524 | View to list all Open Response Assessments (ORAs) for a given course. |
|
0 commit comments