-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Unit extensions API v2 for instructors dashboard #37783
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
holaontiveros
wants to merge
10
commits into
openedx:master
Choose a base branch
from
WGU-Open-edX:feature/unit_extensions_v2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+673
−5
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
6c87142
chore: base work for unit extensions v2 api
holaontiveros a2e3566
chore: fix pylint things
holaontiveros dd4b7eb
chore: update to match edx-when comments
holaontiveros 43deb2a
chore: update edx_when version
holaontiveros 7a98a89
chore: restore old version before upgrade with make
holaontiveros 4913b98
chore: update with make upgrade-package package=edx-when
holaontiveros 55eef0e
chore: exclude extras from get_overrides_for_block to prevent tuple f…
holaontiveros 1b9a193
Merge branch 'master' into feature/unit_extensions_v2
holaontiveros 0e9ea9e
Merge branch 'master' into feature/unit_extensions_v2
dwong2708 9c95a16
feat: address review comments
dwong2708 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,10 +7,14 @@ | |
|
|
||
| import logging | ||
|
|
||
| from dataclasses import dataclass | ||
| from typing import Optional, Tuple | ||
| import edx_api_doc_tools as apidocs | ||
| from edx_when import api as edx_when_api | ||
| from opaque_keys import InvalidKeyError | ||
| from opaque_keys.edx.keys import CourseKey, UsageKey | ||
| from rest_framework import status | ||
| from rest_framework.generics import ListAPIView | ||
| from rest_framework.permissions import IsAuthenticated | ||
| from rest_framework.response import Response | ||
| from rest_framework.views import APIView | ||
|
|
@@ -31,6 +35,7 @@ | |
| InstructorTaskListSerializer, | ||
| CourseInformationSerializerV2, | ||
| BlockDueDateSerializerV2, | ||
| UnitExtensionSerializer, | ||
| ) | ||
| from .tools import ( | ||
| find_unit, | ||
|
|
@@ -349,3 +354,156 @@ def get(self, request, course_id): | |
| } for unit in graded_subsections]} | ||
|
|
||
| return Response(formated_subsections, status=status.HTTP_200_OK) | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class UnitDueDateExtension: | ||
| """Dataclass representing a unit due date extension for a student.""" | ||
|
|
||
| username: str | ||
| full_name: str | ||
| email: str | ||
| unit_title: str | ||
| unit_location: str | ||
| extended_due_date: Optional[str] | ||
|
|
||
| @classmethod | ||
| def from_block_tuple(cls, row: Tuple, unit): | ||
| username, full_name, due_date, email, location = row | ||
| unit_title = title_or_url(unit) | ||
| return cls( | ||
| username=username, | ||
| full_name=full_name, | ||
| email=email, | ||
| unit_title=unit_title, | ||
| unit_location=location, | ||
| extended_due_date=due_date, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def from_course_tuple(cls, row: Tuple, units_dict: dict): | ||
| username, full_name, email, location, due_date = row | ||
| unit_title = title_or_url(units_dict[str(location)]) | ||
| return cls( | ||
| username=username, | ||
| full_name=full_name, | ||
| email=email, | ||
| unit_title=unit_title, | ||
| unit_location=location, | ||
| extended_due_date=due_date, | ||
| ) | ||
|
|
||
|
|
||
| class UnitExtensionsView(ListAPIView): | ||
| """ | ||
| Retrieve a paginated list of due date extensions for units in a course. | ||
|
|
||
| **Example Requests** | ||
|
|
||
| GET /api/instructor/v2/courses/{course_id}/unit_extensions | ||
| GET /api/instructor/v2/courses/{course_id}/unit_extensions?page=2 | ||
| GET /api/instructor/v2/courses/{course_id}/unit_extensions?page_size=50 | ||
| GET /api/instructor/v2/courses/{course_id}/unit_extensions?email_or_username=john | ||
| GET /api/instructor/v2/courses/{course_id}/unit_extensions?block_id=block-v1:org@problem+block@unit1 | ||
|
|
||
| **Response Values** | ||
|
|
||
| { | ||
| "count": 150, | ||
| "next": "http://example.com/api/instructor/v2/courses/course-v1:org+course+run/unit_extensions?page=2", | ||
| "previous": null, | ||
| "results": [ | ||
| { | ||
| "username": "student1", | ||
| "full_name": "John Doe", | ||
| "email": "[email protected]", | ||
| "unit_title": "Unit 1: Introduction", | ||
| "unit_location": "block-v1:org+course+run+type@problem+block@unit1", | ||
| "extended_due_date": "2023-12-25T23:59:59Z" | ||
| }, | ||
| ... | ||
| ] | ||
| } | ||
|
|
||
| **Parameters** | ||
|
|
||
| course_id: Course key for the course. | ||
| page (optional): Page number for pagination. | ||
| page_size (optional): Number of results per page. | ||
|
|
||
| **Returns** | ||
|
|
||
| * 200: OK - Returns paginated list of unit extensions | ||
| * 401: Unauthorized - User is not authenticated | ||
| * 403: Forbidden - User lacks instructor permissions | ||
| * 404: Not Found - Course does not exist | ||
| """ | ||
| permission_classes = (IsAuthenticated, permissions.InstructorPermission) | ||
| permission_name = permissions.VIEW_DASHBOARD | ||
| serializer_class = UnitExtensionSerializer | ||
|
|
||
| def get_queryset(self): | ||
| """ | ||
| Returns the queryset of unit extensions for the specified course. | ||
|
|
||
| This method uses the core logic from get_overrides_for_course to retrieve | ||
| due date extension data and transforms it into a list of normalized objects | ||
| that can be paginated and serialized. | ||
|
|
||
| Supports filtering by: | ||
| - email_or_username: Filter by username or email address | ||
| - block_id: Filter by specific unit/subsection location | ||
| """ | ||
| course_id = self.kwargs["course_id"] | ||
| course_key = CourseKey.from_string(course_id) | ||
| course = get_course_by_id(course_key) | ||
|
|
||
| email_or_username_filter = self.request.query_params.get("email_or_username") | ||
| block_id_filter = self.request.query_params.get("block_id") | ||
|
|
||
| units = get_units_with_due_date(course) | ||
| units_dict = {str(u.location): u for u in units} | ||
|
|
||
| # Fetch and normalize overrides | ||
| if block_id_filter: | ||
| try: | ||
| unit = find_unit(course, block_id_filter) | ||
| query_data = edx_when_api.get_overrides_for_block(course.id, unit.location) | ||
| unit_due_date_extensions = [ | ||
| UnitDueDateExtension.from_block_tuple(row, unit) | ||
| for row in query_data | ||
| ] | ||
| except InvalidKeyError: | ||
| # If block_id is invalid, return empty list | ||
| unit_due_date_extensions = [] | ||
| else: | ||
| query_data = edx_when_api.get_overrides_for_course(course.id) | ||
| unit_due_date_extensions = [ | ||
| UnitDueDateExtension.from_course_tuple(row, units_dict) | ||
| for row in query_data | ||
| if str(row[3]) in units_dict # Ensure unit has due date | ||
| ] | ||
|
|
||
| # Apply filters | ||
| results = [] | ||
| filter_value = email_or_username_filter.lower() if email_or_username_filter else None | ||
|
|
||
| for unit_due_date_extension in unit_due_date_extensions: | ||
| # Optional username/email filter | ||
| if filter_value: | ||
| if ( | ||
| filter_value not in unit_due_date_extension.username.lower() | ||
| and filter_value not in unit_due_date_extension.email.lower() | ||
| ): | ||
| continue | ||
| results.append(unit_due_date_extension) | ||
|
|
||
| # Sort for consistent ordering | ||
| results.sort( | ||
| key=lambda o: ( | ||
| o.username, | ||
| o.unit_title, | ||
| ) | ||
| ) | ||
|
|
||
| return results | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
get_overrides_for_coursehad the same return structure asget_overrides_for_block, I feel like we could clean up the duplication in the if/else blocks. Is it worth adding something (maybe aget_overrides_for_course_v2) to edx-when that does that? Since the two blocks are fairly close, are we able to combine them somehow?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with the suggestion.
We might need an adapter function like this o normalize both responses into the same structure:
and the usage could be: