Skip to content

Commit

Permalink
Basic API view and config to create canvas assignments
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospri committed Sep 29, 2021
1 parent 410c2b2 commit 2d38f0f
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 0 deletions.
13 changes: 13 additions & 0 deletions lms/resources/_js_config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,18 @@ def enable_lti_launch_mode(self):
"allowedOrigins": self._request.registry.settings["rpc_allowed_origins"]
}

def _create_assignment_api(self):
if not self._context.is_canvas:
return None

return {
"path": self._request.route_path("canvas_api.assignments.create"),
"data": {
"ext_lti_assignment_id": self._request.params["ext_lti_assignment_id"],
"course_id": self._request.params["custom_canvas_course_id"],
},
}

def enable_content_item_selection_mode(self, form_action, form_fields):
"""
Put the JavaScript code into "content item selection" mode.
Expand All @@ -212,6 +224,7 @@ def enable_content_item_selection_mode(self, form_action, form_fields):
"filePicker": {
"formAction": form_action,
"formFields": form_fields,
"createAssignmentAPI": self._create_assignment_api(),
# Specific config for pickers
"blackboard": FilePickerConfig.blackboard_config(*args),
"canvas": FilePickerConfig.canvas_config(*args),
Expand Down
3 changes: 3 additions & 0 deletions lms/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def includeme(config):
)

config.add_route("canvas_api.sync", "/api/canvas/sync", request_method="POST")
config.add_route(
"canvas_api.assignments.create", "/api/canvas/assignment", request_method="POST"
)

config.add_route("lti_api.submissions.record", "/api/lti/submissions")
config.add_route("lti_api.result.read", "/api/lti/result", request_method="GET")
Expand Down
1 change: 1 addition & 0 deletions lms/static/scripts/frontend_apps/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { createContext } from 'preact';
* @typedef FilePickerConfig
* @prop {string} formAction
* @prop {Object.<string,string>} formFields
* @prop {APICallInfo} createAssignmentAPI
* @prop {Object} blackboard
* @prop {boolean} blackboard.enabled
* @prop {APICallInfo} blackboard.listFiles
Expand Down
1 change: 1 addition & 0 deletions lms/validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def foo_view(request):
...
"""
from lms.validation._api import (
APICanvasCreateAssignment,
APIReadResultSchema,
APIRecordResultSchema,
APIRecordSpeedgraderSchema,
Expand Down
27 changes: 27 additions & 0 deletions lms/validation/_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Schema for JSON APIs exposed to the frontend."""

import marshmallow
from marshmallow import Schema
from webargs import fields

from lms.validation._base import JSONPyramidRequestSchema, PyramidRequestSchema
Expand Down Expand Up @@ -69,3 +70,29 @@ class APIRecordResultSchema(JSONPyramidRequestSchema):
"""
Score — i.e. grade — for this submission. A value between 0 and 1, inclusive.
"""


class APICanvasCreateAssignment(PyramidRequestSchema):
"""Schema for validating assignment creationg requests made by our frontend."""

class Content(Schema):
class File(Schema):
display_name = fields.Str(required=True)
id = fields.Integer(required=True)
updated_at = fields.String(required=True)
size = fields.Integer(required=True)

type = fields.Str(required=True)
url = fields.Str(required=False, allow_none=True)
file = fields.Nested(File, required=False, allow_none=True)

ext_lti_assignment_id = fields.Str(required=True)
"""Canvas only assignment unique identifier"""

course_id = fields.Integer(required=True)
"""Course ID for the assignment. We'd need it to construct the document_url"""

groupset = fields.Integer(required=False, allow_none=True)
"""Groupset the assignment belongs to if created as a smalls groups assignment"""

content = fields.Nested(Content, required=True)
50 changes: 50 additions & 0 deletions lms/views/api/canvas/assignments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from pyramid.view import view_config, view_defaults

from lms.security import Permissions
from lms.validation import APICanvasCreateAssignment


@view_defaults(request_method="POST", renderer="json", permission=Permissions.API)
class AssignmentsAPIViews:
def __init__(self, request):
self.request = request
self.assignment_service = request.find_service(name="assignment")
self.application_instance = request.find_service(
name="application_instance"
).get()

@view_config(
route_name="canvas_api.assignments.create",
request_method="POST",
schema=APICanvasCreateAssignment,
)
def create(self):
"""
Create an assignment in the DB.
Note that at this point the assignment.resource_link_id == None, it will be filled on first launch.
"""
params = self.request.parsed_params
content = params["content"]
content_type = content["type"]

url = None
extra = {}
if content_type == "url":
url = content["url"]
elif content_type == "file":
url = f"canvas://file/course/{params['course_id']}/file_id/{params['content']['file']['id']}"
extra = {"canvas_file": params["content"]["file"]}
else:
raise ValueError("Unhandled content type on assignment")

if groupset := params.get("groupset"):
extra["canvas_groupset"] = groupset

assignment = self.assignment_service.set_document_url(
self.application_instance.tool_consumer_instance_guid,
url,
ext_lti_assignment_id=params["ext_lti_assignment_id"],
extra=extra,
)
return {"ext_lti_assignment_id": assignment.ext_lti_assignment_id}
1 change: 1 addition & 0 deletions tests/factories/assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
resource_link_id=RESOURCE_LINK_ID,
tool_consumer_instance_guid=TOOL_CONSUMER_INSTANCE_GUID,
document_url=Faker("uri"),
ext_lti_assignment_id=None,
)
41 changes: 41 additions & 0 deletions tests/unit/lms/resources/_js_config/__init___test.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,46 @@ def test_it_adds_picker_config(
application_instance_service.get.return_value,
)

def test_with_create_assignment_api(self, js_config, context):
context.is_canvas = True

js_config.enable_content_item_selection_mode(
mock.sentinel.form_action, mock.sentinel.form_fields
)
config = js_config.asdict()

assert config == Any.dict.containing(
{
"mode": "content-item-selection",
"filePicker": Any.dict.containing(
{
"createAssignmentAPI": {
"path": "/api/canvas/assignment",
"data": {
"ext_lti_assignment_id": "ext_lti_assignment_id",
"course_id": "test_course_id",
},
}
}
),
}
)

def test_with_create_assignment_api_non_canvas(self, js_config, context):
context.is_canvas = False

js_config.enable_content_item_selection_mode(
mock.sentinel.form_action, mock.sentinel.form_fields
)
config = js_config.asdict()

assert config == Any.dict.containing(
{
"mode": "content-item-selection",
"filePicker": Any.dict.containing({"createAssignmentAPI": None}),
}
)

@pytest.fixture(autouse=True)
def FilePickerConfig(self, patch):
return patch("lms.resources._js_config.FilePickerConfig")
Expand Down Expand Up @@ -710,6 +750,7 @@ def pyramid_request(pyramid_request):
"context_id": "test_course_id",
"custom_canvas_course_id": "test_course_id",
"custom_canvas_user_id": "test_user_id",
"ext_lti_assignment_id": "ext_lti_assignment_id",
}
)
return pyramid_request
Expand Down
99 changes: 99 additions & 0 deletions tests/unit/lms/views/api/canvas/assignments_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import pytest

from lms.views.api.canvas.assignments import AssignmentsAPIViews


class TestAssignmentsAPIViews:
@pytest.mark.parametrize(
"request_body,expected_url,expected_extra",
[
(
{
"content": {"type": "url", "url": "https://example.com"},
"ext_lti_assignment_id": "EXT_LTI_ASSIGNMENT_ID",
},
"https://example.com",
{},
),
(
{
"content": {"type": "url", "url": "https://example.com"},
"ext_lti_assignment_id": "EXT_LTI_ASSIGNMENT_ID",
"groupset": 125,
},
"https://example.com",
{"canvas_groupset": 125},
),
(
{
"content": {
"type": "url",
"url": "https://drive.google.com/uc?id=DRIVE_ID&export=download",
},
"ext_lti_assignment_id": "EXT_LTI_ASSIGNMENT_ID",
},
"https://drive.google.com/uc?id=DRIVE_ID&export=download",
{},
),
(
{
"content": {
"type": "file",
"file": {
"size": 205792,
"display_name": "A Third File.pdf",
"id": 652,
"updated_at": "2021-05-17T15:15:47Z",
},
},
"ext_lti_assignment_id": "EXT_LTI_ASSIGNMENT_ID",
"course_id": "COURSE_ID",
},
"canvas://file/course/COURSE_ID/file_id/652",
{
"canvas_file": {
"size": 205792,
"display_name": "A Third File.pdf",
"id": 652,
"updated_at": "2021-05-17T15:15:47Z",
}
},
),
],
)
def test_create(
self,
application_instance_service,
assignment_service,
pyramid_request,
request_body,
expected_url,
expected_extra,
):
pyramid_request.parsed_params = request_body

result = AssignmentsAPIViews(pyramid_request).create()

assignment_service.set_document_url.assert_called_once_with(
application_instance_service.get.return_value.tool_consumer_instance_guid,
expected_url,
ext_lti_assignment_id="EXT_LTI_ASSIGNMENT_ID",
extra=expected_extra,
)
assert result == {
"ext_lti_assignment_id": assignment_service.set_document_url.return_value.ext_lti_assignment_id
}

@pytest.mark.usefixtures("application_instance_service", "assignment_service")
def test_create_unknown_type(
self,
pyramid_request,
):
pyramid_request.parsed_params = {
"content": {"type": "nothing"},
"ext_lti_assignment_id": "EXT_LTI_ASSIGNMENT_ID",
"course_id": "COURSE_ID",
}

with pytest.raises(ValueError):
AssignmentsAPIViews(pyramid_request).create()

0 comments on commit 2d38f0f

Please sign in to comment.