Skip to content

Commit a03cf40

Browse files
George Schneelochasadiqbal
authored andcommitted
Use instructor_tasks for canvas work (#183)
* Add per_page to paginated requests * Static method * Fix email lowercase mismatch * Use instructor_tasks to handle canvas work * Fix transaction error * Decorator needs to be at the top * Attempt to fix polling of tasks * More task bug fixes * Fix typo * Fix course key bug * Update message for push edx grades command * Use course_key so sync_canvas_enrollments uses the same message * Format the submitted time
1 parent fced2e3 commit a03cf40

File tree

13 files changed

+294
-63
lines changed

13 files changed

+294
-63
lines changed

lms/djangoapps/canvas_integration/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def push_edx_grades_to_canvas(course):
150150
for subsection_block, user_grade_dict in subsection_block_user_grades.items():
151151
grade_update_payloads[subsection_block] = dict(
152152
update_grade_payload_kv(
153-
enrolled_user_dict[student_user.email],
153+
enrolled_user_dict[student_user.email.lower()],
154154
grade.percent_graded
155155
)
156156
for student_user, grade in user_grade_dict.items()

lms/djangoapps/canvas_integration/client.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
2-
import time
32
import pytz
4-
from urllib.parse import urljoin
3+
from urllib.parse import urlencode, urljoin, urlparse, parse_qs
54
import requests
65

76
from django.conf import settings
@@ -28,10 +27,31 @@ def get_canvas_session():
2827
})
2928
return session
3029

30+
@staticmethod
31+
def _add_per_page(url, per_page):
32+
"""
33+
Add per_page query parameter to override default value of 10
34+
35+
Args:
36+
url (str): The url to update
37+
per_page (int): The new per_page value
38+
39+
Returns:
40+
str: The updated URL
41+
"""
42+
pieces = urlparse(url)
43+
query = parse_qs(pieces.query)
44+
query['per_page'] = per_page
45+
query_string = urlencode(query, doseq=True)
46+
pieces = pieces._replace(query=query_string)
47+
return pieces.geturl()
48+
3149
def _paginate(self, url, *args, **kwargs):
3250
"""
3351
Iterate over the paginated results of a request
3452
"""
53+
url = self._add_per_page(url, 100) # increase per_page to 100 from default of 10
54+
3555
items = []
3656
while url:
3757
resp = self.session.get(url, *args, **kwargs)
@@ -42,10 +62,6 @@ def _paginate(self, url, *args, **kwargs):
4262
for link in links:
4363
if link["rel"] == "next":
4464
url = link["url"]
45-
# TODO: delay at all? Canvas docs don't say it's necessary, just that the throttled amount is
46-
# respected. If we go over it should trigger a 403 which will raise an exception here.
47-
# 0.2 seconds per request is just a guess to space out requests on the Canvas API
48-
time.sleep(0.2)
4965

5066
return items
5167

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
DEFAULT_ASSIGNMENT_POINTS = 1
2+
TASK_TYPE_SYNC_CANVAS_ENROLLMENTS = "sync_canvas_enrollments"
3+
TASK_TYPE_PUSH_EDX_GRADES_TO_CANVAS = "push_edx_grades_to_canvas"
4+
CANVAS_TASK_TYPES = [
5+
TASK_TYPE_SYNC_CANVAS_ENROLLMENTS,
6+
TASK_TYPE_PUSH_EDX_GRADES_TO_CANVAS,
7+
]
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Helper functions for canvas integration tasks
3+
"""
4+
from time import time
5+
6+
from canvas_integration import api
7+
from courseware.courses import get_course_by_id
8+
from instructor_task.tasks_helper.runner import TaskProgress
9+
10+
11+
def sync_canvas_enrollments(_xmodule_instance_args, _entry_id, course_id, task_input, action_name):
12+
"""Partial function to sync canvas enrollments"""
13+
start_time = time()
14+
num_reports = 1
15+
task_progress = TaskProgress(action_name, num_reports, start_time)
16+
api.sync_canvas_enrollments(
17+
course_key=task_input["course_key"],
18+
canvas_course_id=task_input["canvas_course_id"],
19+
unenroll_current=task_input["unenroll_current"],
20+
)
21+
# for simplicity, only one task update for now when everything is done
22+
return task_progress.update_task_state(extra_meta={
23+
"step": "Done"
24+
})
25+
26+
27+
def push_edx_grades_to_canvas(_xmodule_instance_args, _entry_id, course_id, task_input, action_name):
28+
"""Partial function to push edX grades to canvas"""
29+
start_time = time()
30+
num_reports = 1
31+
task_progress = TaskProgress(action_name, num_reports, start_time)
32+
course = get_course_by_id(course_id)
33+
assignment_grades_updated, created_assignments = api.push_edx_grades_to_canvas(
34+
course=course
35+
)
36+
results = {}
37+
if assignment_grades_updated:
38+
grade_update_results = {}
39+
for subsection_block, grade_update_response in assignment_grades_updated.items():
40+
if grade_update_response.ok:
41+
message = "updated"
42+
else:
43+
message = {"error": grade_update_response.status_code}
44+
grade_update_results[subsection_block.display_name] = message
45+
results["grades"] = grade_update_results
46+
if created_assignments:
47+
created_assignment_results = {}
48+
for subsection_block, new_assignment_response in created_assignments.items():
49+
if new_assignment_response.ok:
50+
message = "created"
51+
else:
52+
message = {"error": new_assignment_response.status_code}
53+
created_assignment_results[subsection_block.display_name] = message
54+
results["assignments"] = created_assignment_results
55+
56+
# for simplicity, only one task update for now when everything is done
57+
return task_progress.update_task_state(extra_meta={
58+
"step": "Done",
59+
"results": results
60+
})
Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,74 @@
11
"""Tasks for canvas"""
2-
3-
import logging
42
import hashlib
3+
import logging
54
from functools import partial
65

76
from celery import task
8-
from django.conf import settings
9-
from django.utils.translation import ugettext_noop
7+
from opaque_keys.edx.locator import CourseLocator
8+
9+
from canvas_integration import task_helpers
10+
from canvas_integration.constants import (
11+
TASK_TYPE_SYNC_CANVAS_ENROLLMENTS,
12+
TASK_TYPE_PUSH_EDX_GRADES_TO_CANVAS,
13+
)
14+
from courseware.courses import get_course_by_id
15+
from instructor_task.api_helper import submit_task
16+
from instructor_task.tasks_base import BaseInstructorTask
17+
from instructor_task.tasks_helper.runner import run_main_task
1018

11-
from canvas_integration import api
1219

1320
TASK_LOG = logging.getLogger('edx.celery.task')
1421

1522

16-
@task
17-
def sync_canvas_enrollments(course_key, canvas_course_id, unenroll_current):
23+
def run_sync_canvas_enrollments(request, course_key, canvas_course_id, unenroll_current):
24+
"""
25+
Submit a task to start syncing canvas enrollments
26+
"""
27+
task_type = TASK_TYPE_SYNC_CANVAS_ENROLLMENTS
28+
task_class = sync_canvas_enrollments_task
29+
task_input = {
30+
"course_key": course_key,
31+
"canvas_course_id": canvas_course_id,
32+
"unenroll_current": unenroll_current
33+
}
34+
task_key = hashlib.md5(course_key.encode("utf8")).hexdigest()
35+
TASK_LOG.debug("Submitting task to sync canvas enrollments")
36+
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
37+
38+
39+
@task(base=BaseInstructorTask)
40+
def sync_canvas_enrollments_task(entry_id, xmodule_instance_args):
1841
"""
1942
Fetch enrollments from canvas and update
43+
"""
44+
action_name = "sync_canvas_enrollments"
45+
TASK_LOG.info("Running task to sync Canvas enrollments")
46+
task_fn = partial(task_helpers.sync_canvas_enrollments, xmodule_instance_args)
47+
return run_main_task(entry_id, task_fn, action_name)
48+
2049

21-
Args:
22-
course_key (str): The edX course key
23-
canvas_course_id (int): The canvas course id
24-
unenroll_current (bool): If true, unenroll existing students TODO
50+
def run_push_edx_grades_to_canvas(request, course_id):
51+
"""
52+
Submit a task to start pushing edX grades to Canvas
53+
"""
54+
task_type = TASK_TYPE_PUSH_EDX_GRADES_TO_CANVAS
55+
task_class = push_edx_grades_to_canvas_task
56+
task_input = {
57+
# course_key is already passed into the task, but we need to put it in task_input as well
58+
# so the instructor task status can be properly calculated instead of being marked incomplete
59+
"course_key": str(course_id)
60+
}
61+
task_key = hashlib.md5(course_id.encode("utf8")).hexdigest()
62+
TASK_LOG.debug("Submitting task to push edX grades to Canvas")
63+
return submit_task(request, task_type, task_class, course_id, task_input, task_key)
64+
65+
66+
@task(base=BaseInstructorTask)
67+
def push_edx_grades_to_canvas_task(entry_id, xmodule_instance_args):
68+
"""
69+
Push edX grades to Canvas
2570
"""
26-
api.sync_canvas_enrollments(
27-
course_key=course_key,
28-
canvas_course_id=canvas_course_id,
29-
unenroll_current=unenroll_current,
30-
)
71+
action_name = "push_edx_grades_to_canvas"
72+
TASK_LOG.info("Running task to push edX grades to Canvas")
73+
task_fn = partial(task_helpers.push_edx_grades_to_canvas, xmodule_instance_args)
74+
return run_main_task(entry_id, task_fn, action_name)
Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
1+
"""Views for canvas integration"""
2+
import logging
3+
14
from django.contrib.auth.models import User
5+
from django.db import transaction
6+
from django.utils.translation import ugettext as _
27
from django.views.decorators.cache import cache_control
38
from django.views.decorators.csrf import ensure_csrf_cookie
49
from django.views.decorators.http import require_POST
510
from opaque_keys.edx.locator import CourseLocator
611

7-
from canvas_integration.client import CanvasClient
12+
from lms.djangoapps.canvas_integration.client import CanvasClient
813
from courseware.courses import get_course_by_id
9-
from canvas_integration import api
14+
from lms.djangoapps.canvas_integration import tasks
15+
from lms.djangoapps.instructor_task.api_helper import AlreadyRunningError
1016
from lms.djangoapps.instructor.views.api import require_course_permission
1117
from lms.djangoapps.instructor import permissions
1218
from student.models import CourseEnrollment, CourseEnrollmentAllowed
1319
from util.json_request import JsonResponse
1420

1521

22+
log = logging.getLogger(__name__)
23+
24+
1625
def _get_edx_enrollment_data(email, course_key):
1726
"""Helper function to look up some info regarding whether a user with a email address is enrolled in edx"""
1827
user = User.objects.filter(email=email).first()
@@ -35,7 +44,6 @@ def list_canvas_enrollments(request, course_id):
3544
course_key = CourseLocator.from_string(course_id)
3645
course = get_course_by_id(course_key)
3746
if not course.canvas_course_id:
38-
# TODO: better exception class?
3947
raise Exception("No canvas_course_id set for course {}".format(course_id))
4048

4149
client = CanvasClient(canvas_course_id=course.canvas_course_id)
@@ -48,6 +56,7 @@ def list_canvas_enrollments(request, course_id):
4856
return JsonResponse(results)
4957

5058

59+
@transaction.non_atomic_requests
5160
@require_POST
5261
@ensure_csrf_cookie
5362
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@@ -60,14 +69,21 @@ def add_canvas_enrollments(request, course_id):
6069
course_key = CourseLocator.from_string(course_id)
6170
course = get_course_by_id(course_key)
6271
if not course.canvas_course_id:
63-
# TODO: better exception class?
6472
raise Exception("No canvas_course_id set for course {}".format(course_id))
65-
api.sync_canvas_enrollments(
66-
course_key=course_id,
67-
canvas_course_id=course.canvas_course_id,
68-
unenroll_current=unenroll_current,
69-
) # WARNING: this will block the web thread
70-
return JsonResponse({"status": "success"})
73+
74+
try:
75+
tasks.run_sync_canvas_enrollments(
76+
request=request,
77+
course_key=course_id,
78+
canvas_course_id=course.canvas_course_id,
79+
unenroll_current=unenroll_current,
80+
)
81+
log.info("Syncing canvas enrollments for course %s", course_id)
82+
success_status = _("Syncing canvas enrollments")
83+
return JsonResponse({"status": success_status})
84+
except AlreadyRunningError:
85+
already_running_status = _("Syncing canvas enrollments. See Pending Tasks below to view the status.")
86+
return JsonResponse({"status": already_running_status})
7187

7288

7389
@ensure_csrf_cookie
@@ -79,7 +95,6 @@ def list_canvas_assignments(request, course_id):
7995
course = get_course_by_id(course_key)
8096
client = CanvasClient(canvas_course_id=course.canvas_course_id)
8197
if not course.canvas_course_id:
82-
# TODO: better exception class?
8398
raise Exception("No canvas_course_id set for course {}".format(course_id))
8499
return JsonResponse(client.list_canvas_assignments())
85100

@@ -94,11 +109,12 @@ def list_canvas_grades(request, course_id):
94109
course = get_course_by_id(course_key)
95110
client = CanvasClient(canvas_course_id=course.canvas_course_id)
96111
if not course.canvas_course_id:
97-
# TODO: better exception class?
98112
raise Exception("No canvas_course_id set for course {}".format(course_id))
99113
return JsonResponse(client.list_canvas_grades(assignment_id=assignment_id))
100114

101115

116+
@transaction.non_atomic_requests
117+
@require_POST
102118
@ensure_csrf_cookie
103119
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
104120
@require_course_permission(permissions.OVERRIDE_GRADES)
@@ -107,29 +123,15 @@ def push_edx_grades(request, course_id):
107123
course_key = CourseLocator.from_string(course_id)
108124
course = get_course_by_id(course_key)
109125
if not course.canvas_course_id:
110-
# TODO: better exception class?
111126
raise Exception("No canvas_course_id set for course {}".format(course_id))
112-
assignment_grades_updated, created_assignments = api.push_edx_grades_to_canvas(
113-
course=course
114-
)
115-
116-
results = {}
117-
if assignment_grades_updated:
118-
grade_update_results = {}
119-
for subsection_block, grade_update_response in assignment_grades_updated.items():
120-
if grade_update_response.ok:
121-
message = "updated"
122-
else:
123-
message = {"error": grade_update_response.status_code}
124-
grade_update_results[subsection_block.display_name] = message
125-
results["grades"] = grade_update_results
126-
if created_assignments:
127-
created_assignment_results = {}
128-
for subsection_block, new_assignment_response in created_assignments.items():
129-
if new_assignment_response.ok:
130-
message = "created"
131-
else:
132-
message = {"error": new_assignment_response.status_code}
133-
created_assignment_results[subsection_block.display_name] = message
134-
results["assignments"] = created_assignment_results
135-
return JsonResponse(results)
127+
try:
128+
tasks.run_push_edx_grades_to_canvas(
129+
request=request,
130+
course_id=course_id,
131+
)
132+
log.info("Pushing edX grades to canvas for course %s", course_id)
133+
success_status = _("Pushing edX grades to canvas")
134+
return JsonResponse({"status": success_status})
135+
except AlreadyRunningError:
136+
already_running_status = _("Pushing edX grades to canvas. See Pending Tasks below to view the status.")
137+
return JsonResponse({"status": already_running_status})

lms/djangoapps/instructor/views/api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,11 @@ def list_instructor_tasks(request, course_id):
19121912
- `problem_location_str` and `unique_student_identifier` lists task
19131913
history for problem AND student (intersection)
19141914
"""
1915+
<<<<<<< HEAD
1916+
=======
1917+
include_remote_gradebook = request.GET.get('include_remote_gradebook') is not None
1918+
include_canvas = request.GET.get('include_canvas') is not None
1919+
>>>>>>> 6bb46a387b (Use instructor_tasks for canvas work (#183))
19151920
course_id = CourseKey.from_string(course_id)
19161921
problem_location_str = strip_if_string(request.POST.get('problem_location_str', False))
19171922
student = request.POST.get('unique_student_identifier', None)
@@ -1934,6 +1939,16 @@ def list_instructor_tasks(request, course_id):
19341939
else:
19351940
# Specifying for single problem's history
19361941
tasks = task_api.get_instructor_task_history(course_id, module_state_key)
1942+
elif include_remote_gradebook:
1943+
tasks = task_api.get_running_instructor_rgb_tasks(
1944+
course_id,
1945+
user=request.user
1946+
)
1947+
elif include_canvas:
1948+
tasks = task_api.get_running_instructor_canvas_tasks(
1949+
course_id,
1950+
user=request.user
1951+
)
19371952
else:
19381953
# If no problem or student, just get currently running tasks
19391954
tasks = task_api.get_running_instructor_tasks(course_id)

lms/djangoapps/instructor/views/instructor_dashboard.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,10 @@ def _section_canvas_integration(course):
710710
"list_canvas_enrollments_url": reverse("list_canvas_enrollments", kwargs={"course_id": course.id}),
711711
"list_canvas_assignments_url": reverse("list_canvas_assignments", kwargs={"course_id": course.id}),
712712
"list_canvas_grades_url": reverse("list_canvas_grades", kwargs={"course_id": course.id}),
713+
'list_instructor_tasks_url': '{}?include_canvas=true'.format(reverse(
714+
'list_instructor_tasks',
715+
kwargs={'course_id': course.id}
716+
)),
713717
"push_edx_grades_url": reverse(
714718
"push_edx_grades", kwargs={"course_id": course.id}
715719
),

0 commit comments

Comments
 (0)