-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: LTI Tool Views feat: Upgrade requirements
- Loading branch information
Showing
35 changed files
with
1,026 additions
and
316 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""REST API.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
"""Test api module.""" | ||
from openedx_lti_tool_plugin.deep_linking.tests import MODULE_PATH | ||
|
||
MODULE_PATH = f'{MODULE_PATH}.api' |
109 changes: 109 additions & 0 deletions
109
openedx_lti_tool_plugin/deep_linking/api/tests/test_views.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
"""Test views module.""" | ||
from unittest.mock import MagicMock, patch | ||
from uuid import uuid4 | ||
|
||
from django.test import TestCase | ||
from pylti1p3.exception import LtiException | ||
from rest_framework.exceptions import APIException | ||
from rest_framework.status import HTTP_400_BAD_REQUEST | ||
|
||
from openedx_lti_tool_plugin.deep_linking.api.tests import MODULE_PATH | ||
from openedx_lti_tool_plugin.deep_linking.api.v1.views import DeepLinkingViewSet | ||
from openedx_lti_tool_plugin.deep_linking.exceptions import DeepLinkingException | ||
|
||
MODULE_PATH = f'{MODULE_PATH}.views' | ||
|
||
|
||
@patch(f'{MODULE_PATH}.validate_deep_linking_message') | ||
@patch(f'{MODULE_PATH}.super') | ||
class TestDeepLinkingViewSet(TestCase): | ||
"""Test DeepLinkingViewSet class.""" | ||
|
||
def setUp(self): | ||
"""Set up test fixtures.""" | ||
super().setUp() | ||
self.view_class = DeepLinkingViewSet | ||
self.view_self = MagicMock() | ||
self.request = MagicMock() | ||
self.launch_id = uuid4() | ||
self.error_message = 'test-error-message' | ||
|
||
def test_initial_with_valid_message( | ||
self, | ||
super_mock: MagicMock, | ||
validate_deep_linking_message_mock: MagicMock, | ||
): | ||
"""Test initial method with valid DjangoMessageLaunch (happy path).""" | ||
self.view_class.initial( | ||
self.view_self, | ||
self.request, | ||
launch_id=self.launch_id, | ||
) | ||
|
||
super_mock.assert_called_once_with() | ||
self.view_self.get_message_from_cache.assert_called_once_with( | ||
self.request, | ||
self.launch_id, | ||
) | ||
validate_deep_linking_message_mock.assert_called_once_with( | ||
self.view_self.get_message_from_cache(), | ||
) | ||
self.view_self.get_message_from_cache().get_launch_data.assert_called_once_with() | ||
self.assertEqual( | ||
self.view_self.launch_data, | ||
self.view_self.get_message_from_cache().get_launch_data(), | ||
) | ||
|
||
def test_initial_with_lti_exception( | ||
self, | ||
super_mock: MagicMock, | ||
validate_deep_linking_message_mock: MagicMock, | ||
): | ||
"""Test initial method with LtiException.""" | ||
self.view_self.get_message_from_cache.side_effect = LtiException( | ||
self.error_message, | ||
) | ||
|
||
with self.assertRaises(APIException) as ctxm: | ||
self.view_class.initial( | ||
self.view_self, | ||
self.request, | ||
launch_id=self.launch_id, | ||
) | ||
|
||
self.assertEqual(self.error_message, str(ctxm.exception)) | ||
self.assertEqual(HTTP_400_BAD_REQUEST, ctxm.exception.detail.code) | ||
super_mock.assert_called_once_with() | ||
self.view_self.get_message_from_cache.assert_called_once_with( | ||
self.request, | ||
self.launch_id, | ||
) | ||
validate_deep_linking_message_mock.assert_not_called() | ||
self.view_self.get_message_from_cache.return_value.get_launch_data.assert_not_called() | ||
|
||
def test_initial_with_deep_linking_exception( | ||
self, | ||
super_mock: MagicMock, | ||
validate_deep_linking_message_mock: MagicMock, | ||
): | ||
"""Test initial method with DeepLinkingException.""" | ||
self.view_self.get_message_from_cache.side_effect = DeepLinkingException( | ||
self.error_message, | ||
) | ||
|
||
with self.assertRaises(APIException) as ctxm: | ||
self.view_class.initial( | ||
self.view_self, | ||
self.request, | ||
launch_id=self.launch_id, | ||
) | ||
|
||
self.assertEqual(self.error_message, str(ctxm.exception)) | ||
self.assertEqual(HTTP_400_BAD_REQUEST, ctxm.exception.detail.code) | ||
super_mock.assert_called_once_with() | ||
self.view_self.get_message_from_cache.assert_called_once_with( | ||
self.request, | ||
self.launch_id, | ||
) | ||
validate_deep_linking_message_mock.assert_not_called() | ||
self.view_self.get_message_from_cache.return_value.get_launch_data.assert_not_called() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
"""Django URL Configuration. | ||
Attributes: | ||
app_name (str): URL pattern namespace. | ||
urlpatterns (list): URL patterns list. | ||
""" | ||
from django.urls import include, path | ||
|
||
app_name = 'api' | ||
urlpatterns = [ | ||
path('v1/', include('openedx_lti_tool_plugin.deep_linking.api.v1.urls')), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""REST API V1.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
"""Pagination.""" | ||
from rest_framework.pagination import PageNumberPagination | ||
|
||
|
||
class ContentItemPagination(PageNumberPagination): | ||
"""Content Item Pagination.""" | ||
|
||
page_size_query_param = 'page_size' | ||
max_page_size = 100 |
33 changes: 33 additions & 0 deletions
33
openedx_lti_tool_plugin/deep_linking/api/v1/serializers.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
"""Django REST Framework Serializers.""" | ||
from rest_framework import serializers | ||
|
||
from openedx_lti_tool_plugin.deep_linking.utils import build_resource_link_launch_url | ||
from openedx_lti_tool_plugin.models import CourseContext | ||
|
||
|
||
class CourseContentItemSerializer(serializers.Serializer): # pylint: disable=abstract-method | ||
"""Course Content Item Serializer. | ||
.. _LTI Deep Linking Specification - Content Item Types: | ||
https://www.imsglobal.org/spec/lti-dl/v2p0#content-item-types | ||
""" | ||
|
||
type = serializers.ReadOnlyField(default='ltiResourceLink') | ||
url = serializers.SerializerMethodField() | ||
title = serializers.CharField(allow_blank=True) | ||
|
||
def get_url(self, course_context: CourseContext): | ||
"""Get Content Item URL. | ||
Args: | ||
course_context: CourseContext object. | ||
Returns: | ||
Course LTI Resource Link Launch URL. | ||
""" | ||
return build_resource_link_launch_url( | ||
self.context.get('request'), | ||
course_context.course_id, | ||
) |
4 changes: 4 additions & 0 deletions
4
openedx_lti_tool_plugin/deep_linking/api/v1/tests/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
"""Test v1 module.""" | ||
from openedx_lti_tool_plugin.deep_linking.api.tests import MODULE_PATH | ||
|
||
MODULE_PATH = f'{MODULE_PATH}.v1' |
21 changes: 21 additions & 0 deletions
21
openedx_lti_tool_plugin/deep_linking/api/v1/tests/test_pagination.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
"""Test pagination module.""" | ||
from django.test import TestCase | ||
|
||
from openedx_lti_tool_plugin.deep_linking.api.v1.pagination import ContentItemPagination | ||
from openedx_lti_tool_plugin.deep_linking.api.v1.tests import MODULE_PATH | ||
|
||
MODULE_PATH = f'{MODULE_PATH}.pagination' | ||
|
||
|
||
class TestContentItemPagination(TestCase): | ||
"""Test ContentItemPagination class.""" | ||
|
||
def setUp(self): | ||
"""Set up test fixtures.""" | ||
super().setUp() | ||
self.pagination_class = ContentItemPagination | ||
|
||
def test_class_attributes(self): | ||
"""Test class attributes.""" | ||
self.assertEqual(self.pagination_class.page_size_query_param, 'page_size') | ||
self.assertEqual(self.pagination_class.max_page_size, 100) |
39 changes: 39 additions & 0 deletions
39
openedx_lti_tool_plugin/deep_linking/api/v1/tests/test_serializers.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
"""Test serializers module.""" | ||
from unittest.mock import MagicMock, patch | ||
|
||
from django.test import TestCase | ||
|
||
from openedx_lti_tool_plugin.deep_linking.api.v1.serializers import CourseContentItemSerializer | ||
from openedx_lti_tool_plugin.deep_linking.api.v1.tests import MODULE_PATH | ||
|
||
MODULE_PATH = f'{MODULE_PATH}.serializers' | ||
|
||
|
||
class TestCourseContentItemSerializer(TestCase): | ||
"""Test CourseContentItemSerializer class.""" | ||
|
||
def setUp(self): | ||
"""Set up test fixtures.""" | ||
super().setUp() | ||
self.serializer_class = CourseContentItemSerializer | ||
self.course_context = MagicMock(course_id=MagicMock()) | ||
self.request = MagicMock() | ||
self.serializer_self = MagicMock(context={'request': self.request}) | ||
|
||
@patch(f'{MODULE_PATH}.build_resource_link_launch_url') | ||
def test_get_url( | ||
self, | ||
build_resource_link_launch_url_mock: MagicMock, | ||
): | ||
"""Test get_url method.""" | ||
self.assertEqual( | ||
self.serializer_class.get_url( | ||
self.serializer_self, | ||
self.course_context, | ||
), | ||
build_resource_link_launch_url_mock.return_value, | ||
) | ||
build_resource_link_launch_url_mock.assert_called_once_with( | ||
self.request, | ||
self.course_context.course_id, | ||
) |
23 changes: 23 additions & 0 deletions
23
openedx_lti_tool_plugin/deep_linking/api/v1/tests/test_urls.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
"""Test urls module.""" | ||
from uuid import uuid4 | ||
|
||
from django.test import TestCase | ||
from django.urls import resolve, reverse | ||
|
||
from openedx_lti_tool_plugin.deep_linking.api.v1.views import CourseContentItemViewSet | ||
|
||
|
||
class TestCourseContentItemViewSetUrlPatterns(TestCase): | ||
"""Test CourseContentItemViewSet Django URL Configuration.""" | ||
|
||
def test_view_url(self): | ||
"""Test View URL.""" | ||
self.assertEqual( | ||
resolve( | ||
reverse( | ||
'1.3:deep-linking:api:v1:course-content-item-list', | ||
args=[uuid4()], | ||
), | ||
).func.cls, | ||
CourseContentItemViewSet, | ||
) |
62 changes: 62 additions & 0 deletions
62
openedx_lti_tool_plugin/deep_linking/api/v1/tests/test_views.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
"""Test views module.""" | ||
from unittest.mock import MagicMock, patch | ||
from uuid import uuid4 | ||
|
||
from django.http.response import Http404 | ||
from django.test import RequestFactory, TestCase, override_settings | ||
from django.urls import reverse | ||
|
||
from openedx_lti_tool_plugin.deep_linking.api.v1.pagination import ContentItemPagination | ||
from openedx_lti_tool_plugin.deep_linking.api.v1.serializers import CourseContentItemSerializer | ||
from openedx_lti_tool_plugin.deep_linking.api.v1.tests import MODULE_PATH | ||
from openedx_lti_tool_plugin.deep_linking.api.v1.views import CourseContentItemViewSet | ||
from openedx_lti_tool_plugin.models import CourseContext | ||
from openedx_lti_tool_plugin.tests import AUD, ISS | ||
|
||
MODULE_PATH = f'{MODULE_PATH}.views' | ||
|
||
|
||
class TestCourseContentItemViewSet(TestCase): | ||
"""Test CourseContentItemViewSet class.""" | ||
|
||
def setUp(self): | ||
"""Set up test fixtures.""" | ||
super().setUp() | ||
self.view_class = CourseContentItemViewSet | ||
self.launch_data = {} | ||
self.view_self = MagicMock(launch_data=self.launch_data) | ||
self.request = RequestFactory().post( | ||
reverse( | ||
'1.3:deep-linking:api:v1:course-content-item-list', | ||
args=[uuid4()], | ||
), | ||
) | ||
|
||
def test_class_attributes(self): | ||
"""Test class attributes.""" | ||
self.assertEqual(self.view_class.serializer_class, CourseContentItemSerializer) | ||
self.assertEqual(self.view_class.pagination_class, ContentItemPagination) | ||
|
||
@patch.object(CourseContext.objects, 'all_for_lti_tool') | ||
@patch(f'{MODULE_PATH}.get_identity_claims') | ||
def test_get_queryset( | ||
self, | ||
get_identity_claims_mock: MagicMock, | ||
all_for_lti_tool_mock: MagicMock, | ||
): | ||
"""Test get_queryset method.""" | ||
get_identity_claims_mock.return_value = ISS, AUD, None, None | ||
|
||
self.assertEqual( | ||
self.view_class.get_queryset(self.view_self), | ||
all_for_lti_tool_mock.return_value.filter_by_site_orgs.return_value, | ||
) | ||
get_identity_claims_mock.assert_called_once_with(self.launch_data) | ||
all_for_lti_tool_mock.assert_called_once_with(ISS, AUD) | ||
all_for_lti_tool_mock().filter_by_site_orgs.assert_called_once_with() | ||
|
||
@override_settings(OLTITP_ENABLE_LTI_TOOL=False) | ||
def test_with_lti_disabled(self): | ||
"""Test raise 404 response when plugin is disabled.""" | ||
with self.assertRaises(Http404): | ||
self.view_class.as_view({'get': 'list'})(self.request) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
"""Django URL Configuration. | ||
Attributes: | ||
app_name (str): URL pattern namespace. | ||
urlpatterns (list): URL patterns list. | ||
""" | ||
from django.urls import path | ||
|
||
from openedx_lti_tool_plugin.deep_linking.api.v1 import views | ||
|
||
app_name = 'v1' | ||
urlpatterns = [ | ||
path( | ||
'<uuid:launch_id>/content_items/courses', | ||
views.CourseContentItemViewSet.as_view({'get': 'list'}), | ||
name='course-content-item-list', | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
"""Django Views.""" | ||
from django.db.models import QuerySet | ||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication | ||
from rest_framework.mixins import ListModelMixin | ||
|
||
from openedx_lti_tool_plugin.deep_linking.api.v1.pagination import ContentItemPagination | ||
from openedx_lti_tool_plugin.deep_linking.api.v1.serializers import CourseContentItemSerializer | ||
from openedx_lti_tool_plugin.deep_linking.api.views import DeepLinkingViewSet | ||
from openedx_lti_tool_plugin.models import CourseContext | ||
from openedx_lti_tool_plugin.utils import get_identity_claims | ||
|
||
|
||
class CourseContentItemViewSet( | ||
ListModelMixin, | ||
DeepLinkingViewSet, | ||
): | ||
"""Course Content Item ViewSet. | ||
A content item is a JSON that represents any content the LTI Platform can consume, | ||
a content item could be an LTI resource link launch URL, a URL to a resource hosted | ||
on the internet, an HTML fragment, or any other kind of content type. | ||
This ViewSet returns a list of LTI Resource Link content items for each Course | ||
available for the LtiTool related to the request launch data and the | ||
site configuration `course_org_filter` setting. | ||
""" | ||
|
||
authentication_classes = (JwtAuthentication,) | ||
serializer_class = CourseContentItemSerializer | ||
pagination_class = ContentItemPagination | ||
|
||
def get_queryset(self) -> QuerySet: | ||
"""Get QuerySet. | ||
Returns: | ||
CourseContext QuerySet. | ||
""" | ||
# Obtain the Issuer and Audience claim from the launch data | ||
# these claims will be used by the CourseContext.all_for_lti_tool method | ||
# to query the pylti1.3 LtiTool model related to this launch data. | ||
iss, aud, _sub, _pii = get_identity_claims(self.launch_data) | ||
|
||
return CourseContext.objects.all_for_lti_tool(iss, aud).filter_by_site_orgs() |
Oops, something went wrong.