Skip to content

Commit a13f6eb

Browse files
committed
feat: sync Canvas due date extensions for students
- Added `sync_canvas_due_date_extensions` to handle due date extensions for specific students in Canvas. - Extended `_sync_canvas_due_dates` to update due dates with overrides. - Added `get_emails_by_student_ids` method in `client.py` to fetch student emails by ID from Canvas. - Updated `list_canvas_assignments` to support fetching overrides with assignments.
1 parent e7a57a2 commit a13f6eb

4 files changed

Lines changed: 72 additions & 13 deletions

File tree

src/ol_openedx_canvas_integration/ol_openedx_canvas_integration/client.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def list_canvas_enrollments(self):
8585
for enrollment in enrollments
8686
}
8787

88-
def list_canvas_assignments(self):
88+
def list_canvas_assignments(self, *args, **kwargs):
8989
"""
9090
List Canvas assignments
9191
@@ -96,7 +96,7 @@ def list_canvas_assignments(self):
9696
settings.CANVAS_BASE_URL,
9797
f"/api/v1/courses/{self.canvas_course_id}/assignments",
9898
)
99-
return self._paginate(url)
99+
return self._paginate(url, *args, **kwargs)
100100

101101
def get_student_id_by_email(self, email: str):
102102
"""
@@ -129,6 +129,24 @@ def get_student_id_by_email(self, email: str):
129129
cache.set(key, student_id)
130130
return student_id
131131

132+
def get_emails_by_student_ids(self, student_ids: list[int]):
133+
"""
134+
Retrieve emails for a list of student IDs from Canvas.
135+
136+
Args:
137+
student_ids (list[int]): List of student IDs.
138+
139+
Returns:
140+
list[str]: List of student emails.
141+
"""
142+
url = urljoin(
143+
settings.CANVAS_BASE_URL,
144+
f"/api/v1/courses/{self.canvas_course_id}/users",
145+
)
146+
results = self._paginate(url, params={"user_ids[]": student_ids})
147+
148+
return [user["login_id"].lower() for user in results if "login_id" in user]
149+
132150
def get_canvas_assignments(self):
133151
"""
134152
Get Canvas assignments organized by integration_id.
@@ -138,12 +156,15 @@ def get_canvas_assignments(self):
138156
'id' and 'is_published' fields. Only includes assignments with
139157
integration_id set.
140158
"""
141-
assignments = self.list_canvas_assignments()
159+
# This query param makes canvas return all overrides for each assignment
160+
query = {"include[]": ["overrides"]}
161+
assignments = self.list_canvas_assignments(params=query)
142162
assignments_dict = {
143163
assignment.get("integration_id"): {
144164
"id": assignment["id"],
145165
"is_published": assignment.get("published", False),
146166
"due_at": assignment.get("due_at"),
167+
"overrides": assignment.get("overrides", []),
147168
}
148169
for assignment in assignments
149170
if assignment.get("integration_id") is not None
@@ -297,4 +318,4 @@ def update_grade_payload_kv(user_id, grade_percent):
297318
Returns:
298319
(tuple): A key/value pair that will be used in the body of a bulk grade update request
299320
""" # noqa: D401, E501
300-
return (f"grade_data[{user_id}][posted_grade]", f"{grade_percent * 100}%")
321+
return f"grade_data[{user_id}][posted_grade]", f"{grade_percent * 100}%"

src/ol_openedx_canvas_integration/ol_openedx_canvas_integration/cms_tasks.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515

1616
import requests
1717
from celery import shared_task
18+
from django.contrib.auth import get_user_model
1819
from django.utils.dateparse import parse_datetime
1920
from lms.djangoapps.courseware.courses import get_course_by_id
21+
from lms.djangoapps.instructor.views.tools import set_due_date_extension
2022
from opaque_keys import InvalidKeyError
2123
from opaque_keys.edx.keys import CourseKey, UsageKey
2224
from opaque_keys.edx.locator import CourseLocator
@@ -33,6 +35,7 @@
3335

3436
logger = logging.getLogger(__name__)
3537
TASK_LOG = logging.getLogger("edx.celery.task")
38+
User = get_user_model()
3639

3740

3841
def diff_assignments(
@@ -215,6 +218,36 @@ def sync_canvas_due_dates(course_id: str):
215218
_sync_canvas_due_dates(course_id)
216219

217220

221+
def sync_canvas_due_date_extensions(client, course, block, overrides):
222+
"""
223+
Synchronize due date extensions for students in Canvas with the platform.
224+
225+
Parameters:
226+
client (CanvasAPIClient): The Canvas API client for making requests.
227+
course (Course): Course object for which due date extensions are being synced.
228+
block (Block): Block object for which due date extensions are being synced.
229+
overrides (list): List of due date overrides from Canvas.
230+
"""
231+
canvas_course_id = get_canvas_course_id(course)
232+
for override in overrides:
233+
if "student_ids" in override:
234+
emails = client.get_emails_by_student_ids(override["student_ids"])
235+
students = User.objects.filter(email__in=emails)
236+
for student in students:
237+
TASK_LOG.info(
238+
"Due Date Sync: Syncing due date for student %s in course %s",
239+
student.username,
240+
course.id,
241+
)
242+
set_due_date_extension(
243+
course,
244+
block,
245+
student,
246+
override["due_at"],
247+
reason=f"Synced from canvas course: {canvas_course_id}",
248+
)
249+
250+
218251
def _sync_canvas_due_dates(course_id: str):
219252
"""
220253
Synchronize assignment due dates from Canvas to a specific course in the platform.
@@ -259,6 +292,9 @@ def _sync_canvas_due_dates(course_id: str):
259292
usage_key = UsageKey.from_string(usage_id)
260293
due_at = canvas_assignment.get("due_at")
261294
block = modulestore().get_item(usage_key)
295+
sync_canvas_due_date_extensions(
296+
client, course, block, canvas_assignment.get("overrides")
297+
)
262298
if due_at:
263299
block.due = parse_datetime(due_at)
264300
else:

src/ol_openedx_canvas_integration/ol_openedx_canvas_integration/tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import hashlib
44
import logging
5-
from datetime import datetime, timezone
5+
from datetime import UTC, datetime
66
from functools import partial
77

88
import requests
@@ -122,7 +122,7 @@ def _sync_user_grade_with_canvas(grade_id):
122122
due_date = parse_datetime(
123123
existing_assignments_map[str(grade_instance.usage_key)]["due_at"]
124124
)
125-
if due_date and due_date < datetime.now(tz=timezone.utc):
125+
if due_date and due_date < datetime.now(tz=UTC):
126126
TASK_LOG.warning(
127127
"The assignment %s is past its due date. Skipping grade sync.",
128128
grade_instance.usage_key,

src/ol_openedx_canvas_integration/tests/test_tasks.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
import requests
77
from django.contrib.auth import get_user_model
8-
from django.test import override_settings, TestCase
9-
from opaque_keys.edx.keys import UsageKey, CourseKey
10-
11-
from ol_openedx_canvas_integration.tasks import _sync_user_grade_with_canvas
8+
from django.test import TestCase, override_settings
129
from lms.djangoapps.grades.models import PersistentSubsectionGrade
10+
from ol_openedx_canvas_integration.tasks import _sync_user_grade_with_canvas
11+
from opaque_keys.edx.keys import CourseKey, UsageKey
1312
from openedx.core.djangolib.testing.utils import skip_unless_lms
13+
from pytz import UTC
1414

1515
USER_MODEL = get_user_model()
1616

@@ -37,7 +37,9 @@ def setUp(self):
3737
self.canvas_user_id = 456
3838
self.canvas_assignment_id = 789
3939
self.user = USER_MODEL.objects.create_user(
40-
username="student", email="student@example.com", password="password"
40+
username="student",
41+
email="student@example.com",
42+
password="password", # noqa: S106 # pragma: allowlist secret
4143
)
4244
self.grade_instance = PersistentSubsectionGrade.update_or_create_grade(
4345
user_id=self.user.id,
@@ -49,7 +51,7 @@ def setUp(self):
4951
earned_graded=6.0,
5052
possible_graded=8.0,
5153
visible_blocks=[],
52-
first_attempted=datetime.now(),
54+
first_attempted=datetime.now(tz=UTC),
5355
)
5456
# Mock Course
5557
self.course = MagicMock()
@@ -138,7 +140,7 @@ def test_assignment_past_due_date(
138140
mock_client.get_canvas_assignments.return_value = {
139141
"dummy-key": {
140142
"id": self.canvas_assignment_id,
141-
"due_at": str(datetime.now() - timedelta(days=1)),
143+
"due_at": str(datetime.now(tz=UTC) - timedelta(days=1)),
142144
}
143145
}
144146

0 commit comments

Comments
 (0)