-
Notifications
You must be signed in to change notification settings - Fork 14
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
Suppport for D2L files proxing using Via #4792
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,6 +53,9 @@ def includeme(config): # pylint:disable=too-many-statements | |
"d2l_api.courses.group_sets.list", "/api/d2l/courses/{course_id}/group_sets" | ||
) | ||
config.add_route("d2l_api.courses.files.list", "/api/d2l/courses/{course_id}/files") | ||
config.add_route( | ||
"d2l_api.courses.files.via_url", "/api/d2l/courses/{course_id}/via_url" | ||
) | ||
Comment on lines
+56
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And the new Via URL API for the frontend to call go get D2L files Via URLs 👍 |
||
|
||
config.add_route( | ||
"blackboard_api.oauth.authorize", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -117,7 +117,33 @@ def list_files(self, org_unit) -> List[dict]: | |
"""Get a nested list of files and folders for the given `org_unit`.""" | ||
|
||
modules = self._get_course_modules(org_unit) | ||
return list(self._find_files(modules)) | ||
return list(self._find_files(org_unit, modules)) | ||
|
||
def public_url(self, org_unit, file_id) -> str: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Service method for getting a public URL for a D2L file. So far this is all very familiar 😄 |
||
""" | ||
Return the URL to download the given file. | ||
|
||
As opposed to other LMS's that return a one time signed URL that we can then pass along to Via | ||
D2L requires us to send the API authentication header to get the file contents. | ||
|
||
To make sure the API authentication header is not expired we'll make a API | ||
request here so if it needs to be refreshed we follow the standard token refresh procedure. | ||
|
||
https://docs.valence.desire2learn.com/res/content.html#get--d2l-api-le-(version)-(orgUnitId)-content-topics-(topicId) | ||
https://docs.valence.desire2learn.com/res/content.html#get--d2l-api-le-(version)-(orgUnitId)-content-topics-(topicId)-file | ||
Comment on lines
+129
to
+133
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is probably better in code comments next to the code that makes these requests. Otherwise it seems likely that if the code changes this docstring will get out of date. |
||
""" | ||
|
||
# We don't need the data from this call. | ||
# We are only interested on the potential side effect of needing | ||
# a new access token and/or refreshing an existing one. | ||
_ = self._api.request( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit of a hack but the whole setup is (sending the header etc). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this because we're going to be sending our access token to Via (in the new secret headers), and we therefore need to make sure that the access token is still valid first? |
||
"GET", | ||
self._api.api_url(f"/{org_unit}/content/topics/{file_id}", product="le"), | ||
) | ||
|
||
return self._api.api_url( | ||
f"/{org_unit}/content/topics/{file_id}/file?stream=1", product="le" | ||
) | ||
|
||
@staticmethod | ||
def get_api_user_id(user_id: str): | ||
|
@@ -145,21 +171,21 @@ def _get_course_modules(self, org_unit) -> List[dict]: | |
) | ||
return D2LTableOfContentsSchema(response).parse().get("modules", []) | ||
|
||
def _find_files(self, modules): | ||
def _find_files(self, course_id, modules): | ||
"""Recursively find files in modules.""" | ||
for module in modules: | ||
module_files = [ | ||
{ | ||
"type": "File", | ||
"display_name": topic["display_name"], | ||
"id": topic["id"], | ||
"id": f"d2l://file/course/{course_id}/file_id/{topic['id']}/", | ||
"updated_at": topic["updated_at"], | ||
} | ||
for topic in module.get("topics", []) | ||
if topic.get("type") == "File" | ||
] | ||
|
||
module_children = self._find_files(module.get("modules", [])) | ||
module_children = self._find_files(course_id, module.get("modules", [])) | ||
yield { | ||
"type": "Folder", | ||
"display_name": module["display_name"], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ | |
from lms.validation.authentication import OAuthCallbackSchema | ||
|
||
GROUPS_SCOPES = ("groups:*:*",) | ||
FILES_SCOPES = ("content:toc:read",) | ||
FILES_SCOPES = ("content:toc:read", "content:topics:read") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder what will happen to people who've already granted us access tokens without the topics scope? Will they get prompted to re-authorize us? Maybe no real users are using this feature yet anyway? |
||
|
||
|
||
@view_config( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,14 @@ | ||
import re | ||
|
||
from pyramid.view import view_config | ||
|
||
from lms.security import Permissions | ||
from lms.services.d2l_api import D2LAPIClient | ||
from lms.views import helpers | ||
|
||
DOCUMENT_URL_REGEX = re.compile( | ||
r"d2l:\/\/file\/course\/(?P<course_id>[^\/]*)\/file_id\/(?P<file_id>[^\/]*)\/" | ||
) | ||
|
||
|
||
@view_config( | ||
|
@@ -12,6 +19,27 @@ | |
) | ||
def list_files(_context, request): | ||
"""Return the list of files in the given course.""" | ||
return request.find_service(D2LAPIClient).list_files( | ||
org_unit=request.matchdict["course_id"] | ||
) | ||
return request.find_service(D2LAPIClient).list_files(request.matchdict["course_id"]) | ||
|
||
|
||
@view_config( | ||
request_method="GET", | ||
route_name="d2l_api.courses.files.via_url", | ||
renderer="json", | ||
permission=Permissions.API, | ||
) | ||
def via_url(_context, request): | ||
course_id = request.matchdict["course_id"] | ||
document_url = request.params["document_url"] | ||
file_id = DOCUMENT_URL_REGEX.search(document_url)["file_id"] | ||
|
||
public_url = request.find_service(D2LAPIClient).public_url(course_id, file_id) | ||
|
||
access_token = request.find_service(name="oauth2_token").get().access_token | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought I might need to get this from a private method in the client but we can just use the oauth2 service 👍 |
||
headers = {"Authorization": f"Bearer {access_token}"} | ||
|
||
return { | ||
"via_url": helpers.via_url( | ||
request, public_url, content_type="pdf", headers=headers | ||
) | ||
} |
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.
Ah ok, this is the config telling the frontend to make the API call to get the Via URL? 👍