Skip to content
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

refactor: move more functionality to Canvas plugin #387

Merged
merged 5 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ol_openedx_canvas_integration/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ python_distribution(
],
provides=setup_py(
name="ol-openedx-canvas-integration",
version="0.3.0",
version="0.4.0",
description="An Open edX plugin to add canvas integration support",
license="BSD-3-Clause",
entry_points={
Expand Down
29 changes: 23 additions & 6 deletions src/ol_openedx_canvas_integration/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ We had to make some changes to edx-platform itself in order to add the "Canvas"

The ``edx-platform`` branch/tag you're using must include one of the below commit for ``ol-openedx-canvas-integration`` plugin to work properly:

**For "Nutmeg" or more recent release of edX platform, you should cherry-pick below commit:**
**For "Sumac" or more recent release of edX platform, you should cherry-pick below commit:**

TBA - Will be added when we merge the PR in edx-platform

**For "Quince" to "Redwood" release of edX platform, you should cherry-pick below commit:**

https://github.com/mitodl/edx-platform/commit/7a2edd5d29ead6845cb33d2001746207cf696383

**For "Nutmeg" to "Palm" release of edX platform, you should cherry-pick below commit:**

- https://github.com/mitodl/edx-platform/pull/297/commits/c354a99bd14393b89a780692d07b6e70b586d172

Expand All @@ -20,10 +28,18 @@ The ``edx-platform`` branch/tag you're using must include one of the below commi

Version Compatibility
---------------------
**For "Nutmeg" or more recent release of edX platform**

Use ``0.2.4`` or a above version of this plugin
**For "Sumac" or more recent release of edX platform**

Use ``0.4.0`` or a above version of this plugin

**For "Quince" to "Redwood" release of edX platform**

Use ``0.3.0`` or a above version of this plugin

**For "Nutmeg" to "Palm" release of edX platform**

Use ``0.2.4`` or a above version of this plugin

**For releases prior to "Nutmeg"**

Expand All @@ -49,8 +65,8 @@ You can install this plugin into any Open edX instance by using any of the follo
Follow these steps in a terminal on your machine:

1. Navigate to ``open-edx-plugins`` directory
2. If you haven't done so already, run ``./pants build``
3. Run ``./pants package ::``. This will create a "dist" directory inside "open-edx-plugins" directory with ".whl" & ".tar.gz" format packages for all the "ol_openedx_*" plugins in "open-edx-plugins/src")
2. If you haven't done so already, run ``pants build``
3. Run ``pants package ::``. This will create a "dist" directory inside "open-edx-plugins" directory with ".whl" & ".tar.gz" format packages for all the "ol_openedx_*" plugins in "open-edx-plugins/src")
4. Move/copy any of the ".whl" or ".tar.gz" files for this plugin that were generated in the above step to the machine/container running Open edX (NOTE: If running devstack via Docker, you can use ``docker cp`` to copy these files into your LMS container)
5. Run a shell in the machine/container running Open edX, and install this plugin using pip

Expand All @@ -71,7 +87,8 @@ Add the following configuration values to the config file in Open edX. For any r

1) Open your course in Studio.
2) Navigate to "Advanced Settings".
3) Add a ``canvas_course_id`` value. This should be the id of a course that exists on Canvas. (NOTE: Canvas tab would only be visible if this value is set)
3) Enable other course settings by enabling ``ENABLE_OTHER_COURSE_SETTINGS`` feature flag in CMS
4) Open course advanced settings in Open edX CMS, Add a dictionary in ``{"canvas_id": <canvas_course_id>}``. The ``canvas_course_id`` should be the id of a course that exists on Canvas. (NOTE: Canvas tab would only be visible if this value is set)


How To Use
Expand Down
24 changes: 20 additions & 4 deletions src/ol_openedx_canvas_integration/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
create_assignment_payload,
update_grade_payload_kv,
)
from ol_openedx_canvas_integration.constants import COURSE_KEY_ID_EMPTY
from ol_openedx_canvas_integration.utils import get_canvas_course_id
from opaque_keys.edx.locator import CourseLocator

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -156,13 +158,20 @@ def sync_canvas_enrollments(course_key, canvas_course_id, unenroll_current):
canvas_course_id (int): The canvas course id
unenroll_current (bool): If true, unenroll existing students if not staff
"""
client = CanvasClient(canvas_course_id)
emails_to_enroll = client.list_canvas_enrollments()
users_to_unenroll = []
if not course_key:
raise Exception(COURSE_KEY_ID_EMPTY) # noqa: TRY002

if not canvas_course_id:
msg = f"No canvas_course_id set for course: {course_key}"
raise Exception(msg) # noqa: TRY002

course_key = CourseLocator.from_string(course_key)
course = get_course_by_id(course_key)

client = CanvasClient(canvas_course_id)
emails_to_enroll = client.list_canvas_enrollments()
users_to_unenroll = []

if unenroll_current:
enrolled_user_dict = {
user.email: user for user in get_enrolled_non_staff_users(course)
Expand Down Expand Up @@ -197,7 +206,14 @@ def push_edx_grades_to_canvas(course):
Returns:
dict: A dictionary with some information about the success/failure of the updates
""" # noqa: E501
canvas_course_id = course.canvas_course_id
if not course:
raise Exception(COURSE_KEY_ID_EMPTY) # noqa: TRY002

canvas_course_id = get_canvas_course_id(course)
if not canvas_course_id:
msg = f"No canvas_course_id set for course: {course.id}"
raise Exception(msg) # noqa: TRY002

client = CanvasClient(canvas_course_id=canvas_course_id)
existing_assignment_dict = client.get_assignments_by_int_id()
subsection_block_user_grades = get_subsection_block_user_grades(course)
Expand Down
1 change: 1 addition & 0 deletions src/ol_openedx_canvas_integration/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
TASK_TYPE_SYNC_CANVAS_ENROLLMENTS,
TASK_TYPE_PUSH_EDX_GRADES_TO_CANVAS,
]
COURSE_KEY_ID_EMPTY = "Course Key/ID is empty."
3 changes: 2 additions & 1 deletion src/ol_openedx_canvas_integration/context_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pkg_resources
from django.urls import reverse
from django.utils.translation import gettext as _
from ol_openedx_canvas_integration.utils import get_canvas_course_id
from web_fragments.fragment import Fragment


Expand All @@ -29,7 +30,7 @@ def plugin_context(context):

# Don't add Canvas tab is the Instructor Dashboard if it doesn't have any associated
# canvas_course_id set from Canvas Service
if not course.canvas_course_id:
if not get_canvas_course_id(course):
return None

fragment = Fragment()
Expand Down
23 changes: 23 additions & 0 deletions src/ol_openedx_canvas_integration/task_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
Helper functions for canvas integration tasks
"""

import datetime
from time import time

import pytz
from lms.djangoapps.courseware.courses import get_course_by_id
from lms.djangoapps.instructor_task.api import get_running_instructor_tasks
from lms.djangoapps.instructor_task.models import InstructorTask
from lms.djangoapps.instructor_task.tasks_helper.runner import TaskProgress
from ol_openedx_canvas_integration import api
from ol_openedx_canvas_integration.constants import CANVAS_TASK_TYPES


def sync_canvas_enrollments(
Expand Down Expand Up @@ -46,3 +51,21 @@ def push_edx_grades_to_canvas(
return task_progress.update_task_state(
extra_meta={"step": "Done", "results": results}
)


def get_filtered_instructor_tasks(course_id, user):
"""
Return a filtered query of InstructorTasks based on the course, user, and desired
task types
"""
instructor_tasks = get_running_instructor_tasks(course_id)
now = datetime.datetime.now(pytz.utc)
filtered_tasks = InstructorTask.objects.filter(
course_id=course_id,
task_type__in=CANVAS_TASK_TYPES,
updated__lte=now,
updated__gte=now - datetime.timedelta(days=2),
requester=user,
).order_by("-updated")

return (instructor_tasks | filtered_tasks).distinct()[0:3]
18 changes: 18 additions & 0 deletions src/ol_openedx_canvas_integration/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Utilities for Canvas plugin"""


def get_canvas_course_id(course=None):
"""Get the course Id from the course settings"""
return course.other_course_settings.get("canvas_id") if course else None


def get_task_output_formatted_message(task_output):
"""Take the edX task output and format a message for table display on task result"""
# this reports on actions for a course as a whole
results = task_output.get("results", {})
assignments_count = results.get("assignments", 0)
grades_count = results.get("grades", 0)

return (
f"{grades_count} grades and {assignments_count} assignments updated or created"
)
46 changes: 34 additions & 12 deletions src/ol_openedx_canvas_integration/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from lms.djangoapps.instructor_task.api_helper import AlreadyRunningError
from ol_openedx_canvas_integration import tasks
from ol_openedx_canvas_integration.client import CanvasClient
from ol_openedx_canvas_integration.constants import COURSE_KEY_ID_EMPTY
from ol_openedx_canvas_integration.utils import get_canvas_course_id
from opaque_keys.edx.locator import CourseLocator

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -44,13 +46,18 @@ def list_canvas_enrollments(request, course_id): # noqa: ARG001
"""
Fetch enrollees for a course in canvas and list them
"""
if not course_id:
raise Exception(COURSE_KEY_ID_EMPTY) # noqa: TRY002

course_key = CourseLocator.from_string(course_id)
course = get_course_by_id(course_key)
if not course.canvas_course_id:
msg = f"No canvas_course_id set for course {course_id}"
canvas_course_id = get_canvas_course_id(course)

if not canvas_course_id:
msg = f"No canvas_course_id set for course: {course_id}"
raise Exception(msg) # noqa: TRY002

client = CanvasClient(canvas_course_id=course.canvas_course_id)
client = CanvasClient(canvas_course_id=canvas_course_id)
# WARNING: this will block the web thread
enrollment_dict = client.list_canvas_enrollments()

Expand All @@ -73,15 +80,16 @@ def add_canvas_enrollments(request, course_id):
unenroll_current = request.POST.get("unenroll_current", "").lower() == "true"
course_key = CourseLocator.from_string(course_id)
course = get_course_by_id(course_key)
if not course.canvas_course_id:
canvas_course_id = get_canvas_course_id(course)
if not canvas_course_id:
msg = f"No canvas_course_id set for course {course_id}"
raise Exception(msg) # noqa: TRY002

try:
tasks.run_sync_canvas_enrollments(
request=request,
course_key=course_id,
canvas_course_id=course.canvas_course_id,
canvas_course_id=canvas_course_id,
unenroll_current=unenroll_current,
)
log.info("Syncing canvas enrollments for course %s", course_id)
Expand All @@ -99,12 +107,17 @@ def add_canvas_enrollments(request, course_id):
@require_course_permission(permissions.OVERRIDE_GRADES)
def list_canvas_assignments(request, course_id): # noqa: ARG001
"""List Canvas assignments"""
if not course_id:
raise Exception(COURSE_KEY_ID_EMPTY) # noqa: TRY002

course_key = CourseLocator.from_string(course_id)
course = get_course_by_id(course_key)
client = CanvasClient(canvas_course_id=course.canvas_course_id)
if not course.canvas_course_id:
msg = f"No canvas_course_id set for course {course_id}"
canvas_course_id = get_canvas_course_id(course)
if not canvas_course_id:
msg = f"No canvas_course_id set for course: {course_id}"
raise Exception(msg) # noqa: TRY002

client = CanvasClient(canvas_course_id=canvas_course_id)
return JsonResponse(client.list_canvas_assignments())


Expand All @@ -113,13 +126,18 @@ def list_canvas_assignments(request, course_id): # noqa: ARG001
@require_course_permission(permissions.OVERRIDE_GRADES)
def list_canvas_grades(request, course_id):
"""List grades"""
if not course_id:
raise Exception(COURSE_KEY_ID_EMPTY) # noqa: TRY002

assignment_id = int(request.GET.get("assignment_id"))
course_key = CourseLocator.from_string(course_id)
course = get_course_by_id(course_key)
client = CanvasClient(canvas_course_id=course.canvas_course_id)
if not course.canvas_course_id:
canvas_course_id = get_canvas_course_id(course)
if not canvas_course_id:
msg = f"No canvas_course_id set for course {course_id}"
raise Exception(msg) # noqa: TRY002

client = CanvasClient(canvas_course_id=canvas_course_id)
return JsonResponse(client.list_canvas_grades(assignment_id=assignment_id))


Expand All @@ -130,10 +148,14 @@ def list_canvas_grades(request, course_id):
@require_course_permission(permissions.OVERRIDE_GRADES)
def push_edx_grades(request, course_id):
"""Push user grades for all graded items in edX to Canvas"""
if not course_id:
raise Exception(COURSE_KEY_ID_EMPTY) # noqa: TRY002

course_key = CourseLocator.from_string(course_id)
course = get_course_by_id(course_key)
if not course.canvas_course_id:
msg = f"No canvas_course_id set for course {course_id}"
canvas_course_id = get_canvas_course_id(course)
if not canvas_course_id:
msg = f"No canvas_course_id set for course: {course_id}"
raise Exception(msg) # noqa: TRY002
try:
tasks.run_push_edx_grades_to_canvas(
Expand Down
Loading