Skip to content

Commit

Permalink
Merge pull request #35 from Pearson-Advance/vue/PADV-1191
Browse files Browse the repository at this point in the history
PADV-1191: Add DeepLinkingForm to DeepLinkingView.
  • Loading branch information
kuipumu authored May 22, 2024
2 parents bf91a45 + 08015cf commit 234bf29
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 13 deletions.
88 changes: 88 additions & 0 deletions openedx_lti_tool_plugin/deep_linking/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Django Forms."""
from typing import List, Optional, Set, Tuple

from django import forms
from django.http.request import HttpRequest
from django.urls import reverse
from django.utils.translation import gettext as _
from pylti1p3.deep_link_resource import DeepLinkResource

from openedx_lti_tool_plugin.apps import OpenEdxLtiToolPluginConfig as app_config
from openedx_lti_tool_plugin.edxapp_wrapper.learning_sequences import course_context


class DeepLinkingForm(forms.Form):
"""Deep Linking Form."""

def __init__(self, *args: tuple, request=None, **kwargs: dict):
"""Class __init__ method.
Initialize class instance attributes and add `content_items` field.
Args:
*args: Variable length argument list.
request: HttpRequest object.
**kwargs: Arbitrary keyword arguments.
"""
super().__init__(*args, **kwargs)
self.fields['content_items'] = forms.MultipleChoiceField(
choices=self.get_content_items_choices(request),
required=False,
widget=forms.CheckboxSelectMultiple,
label=_('Courses'),
)

def get_content_items_choices(self, request: HttpRequest) -> List[Tuple[str, str]]:
"""Get `content_items` field choices.
Args:
request: HttpRequest object.
Returns:
List of tuples with choices for the `content_items` field.
"""
return [
self.get_content_items_choice(course, request)
for course in course_context().objects.all()
]

def get_content_items_choice(self, course, request: HttpRequest) -> Tuple[str, str]:
"""Get `content_items` field choice.
Args:
course (CourseContext): Course object.
request: HttpRequest object.
Returns:
Tuple containing the choice value and name.
.. _LTI Deep Linking Specification - LTI Resource Link:
https://www.imsglobal.org/spec/lti-dl/v2p0#lti-resource-link
"""
relative_url = reverse(
f'{app_config.name}:1.3:resource-link:launch-course',
kwargs={'course_id': course.learning_context.context_key},
)

return (
request.build_absolute_uri(relative_url),
course.learning_context.title,
)

def get_deep_link_resources(self) -> Set[Optional[DeepLinkResource]]:
"""Get DeepLinkResource objects from this form `cleaned_data` attribute.
Returns:
Set of DeepLinkResource objects or an empty set
.. _LTI 1.3 Advantage Tool implementation in Python - LTI Message Launches:
https://github.com/dmitry-viskov/pylti1.3?tab=readme-ov-file#deep-linking-responses
"""
return {
DeepLinkResource().set_url(content_item)
for content_item in self.cleaned_data.get('content_items', [])
}
106 changes: 106 additions & 0 deletions openedx_lti_tool_plugin/deep_linking/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Tests forms module."""
from unittest.mock import MagicMock, patch

from django import forms
from django.test import TestCase

from openedx_lti_tool_plugin.apps import OpenEdxLtiToolPluginConfig as app_config
from openedx_lti_tool_plugin.deep_linking.forms import DeepLinkingForm
from openedx_lti_tool_plugin.deep_linking.tests import MODULE_PATH

MODULE_PATH = f'{MODULE_PATH}.forms'


class TestDeepLinkingForm(TestCase):
"""Test DeepLinkingForm class."""

def setUp(self):
"""Set up test fixtures."""
super().setUp()
self.form_class = DeepLinkingForm
self.request = MagicMock()
self.learning_context = MagicMock(context_key='random-course-key', title='Test')
self.course = MagicMock(learning_context=self.learning_context)

@patch(f'{MODULE_PATH}.forms.MultipleChoiceField')
@patch(f'{MODULE_PATH}._')
@patch.object(DeepLinkingForm, 'get_content_items_choices')
def test_init(
self,
get_content_items_choices_mock: MagicMock,
gettext_mock: MagicMock,
multiple_choice_field_mock: MagicMock,
):
"""Test `__init__` method."""
self.assertEqual(
self.form_class(request=self.request).fields,
{'content_items': multiple_choice_field_mock.return_value},
)
get_content_items_choices_mock.assert_called_once_with(self.request)
gettext_mock.assert_called_once_with('Courses')
multiple_choice_field_mock.assert_called_once_with(
choices=get_content_items_choices_mock(),
required=False,
widget=forms.CheckboxSelectMultiple,
label=gettext_mock(),
)

@patch(f'{MODULE_PATH}.course_context')
@patch.object(DeepLinkingForm, 'get_content_items_choice')
@patch.object(DeepLinkingForm, '__init__', return_value=None)
def test_get_content_items_choices(
self,
deep_linking_form_init: MagicMock, # pylint: disable=unused-argument
get_content_items_choice_mock: MagicMock,
course_context_mock: MagicMock,
):
"""Test `get_content_items_choices` method."""
course_context_mock.return_value.objects.all.return_value = [self.course]

self.assertEqual(
self.form_class().get_content_items_choices(self.request),
[get_content_items_choice_mock.return_value],
)
course_context_mock.assert_called_once_with()
course_context_mock().objects.all.assert_called_once_with()
get_content_items_choice_mock.assert_called_once_with(self.course, self.request)

@patch(f'{MODULE_PATH}.reverse')
@patch.object(DeepLinkingForm, '__init__', return_value=None)
def test_get_content_items_choice(
self,
deep_linking_form_init: MagicMock, # pylint: disable=unused-argument
reverse_mock: MagicMock,
):
"""Test `get_content_items_choice` method."""
self.assertEqual(
self.form_class().get_content_items_choice(self.course, self.request),
(
self.request.build_absolute_uri.return_value,
self.course.learning_context.title,
),
)
reverse_mock.assert_called_once_with(
f'{app_config.name}:1.3:resource-link:launch-course',
kwargs={'course_id': self.course.learning_context.context_key},
)
self.request.build_absolute_uri.assert_called_once_with(reverse_mock())

@patch(f'{MODULE_PATH}.DeepLinkResource')
@patch.object(DeepLinkingForm, '__init__', return_value=None)
def test_get_deep_link_resources(
self,
deep_linking_form_init: MagicMock, # pylint: disable=unused-argument
deep_link_resource_mock: MagicMock,
):
"""Test `get_deep_link_resources` method."""
content_item = 'https://example.com'
form = self.form_class()
form.cleaned_data = {'content_items': [content_item]}

self.assertEqual(
form.get_deep_link_resources(),
{deep_link_resource_mock.return_value.set_url.return_value},
)
deep_link_resource_mock.assert_called_once_with()
deep_link_resource_mock().set_url.assert_called_once_with(content_item)
75 changes: 70 additions & 5 deletions openedx_lti_tool_plugin/deep_linking/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from openedx_lti_tool_plugin.apps import OpenEdxLtiToolPluginConfig as app_config
from openedx_lti_tool_plugin.deep_linking.exceptions import DeepLinkingException
from openedx_lti_tool_plugin.deep_linking.forms import DeepLinkingForm
from openedx_lti_tool_plugin.deep_linking.tests import MODULE_PATH
from openedx_lti_tool_plugin.deep_linking.views import (
DeepLinkingFormView,
Expand Down Expand Up @@ -144,8 +145,17 @@ def test_raises_deep_linking_exception(
http_response_error_mock.assert_called_once_with(exception)


class TestDeepLinkingFormView(TestCase):
"""Test DeepLinkingFormView."""

def test_class_attributes(self):
"""Test class attributes."""
self.assertEqual(DeepLinkingFormView.form_class, DeepLinkingForm)


@patch.object(DeepLinkingFormView, 'get_message_from_cache')
@patch(f'{MODULE_PATH}.validate_deep_linking_message')
@patch.object(DeepLinkingFormView, 'form_class')
class TestDeepLinkingFormViewGet(TestCase):
"""Test DeepLinkingFormView get method."""

Expand All @@ -158,26 +168,37 @@ def setUp(self):
self.url = reverse('1.3:deep-linking:form', args=[self.launch_id])
self.request = self.factory.get(self.url)

@patch(f'{MODULE_PATH}.HttpResponse')
@patch(f'{MODULE_PATH}.render')
def test_get(
self,
http_response_mock: MagicMock,
render_mock: MagicMock,
form_class_mock: MagicMock,
validate_deep_linking_message_mock: MagicMock,
get_message_from_cache_mock: MagicMock,
):
"""Test `get` method with valid launch_id (happy path)."""
self.assertEqual(
self.view_class.as_view()(self.request, self.launch_id),
http_response_mock.return_value,
render_mock.return_value,
)
get_message_from_cache_mock.assert_called_once_with(self.request, self.launch_id)
validate_deep_linking_message_mock.assert_called_once_with(get_message_from_cache_mock())
http_response_mock.assert_called_once_with()
form_class_mock.assert_called_once_with(request=self.request)
render_mock.assert_called_once_with(
self.request,
'openedx_lti_tool_plugin/deep_linking/form.html',
{
'form': form_class_mock(),
'form_url': f'{app_config.name}:1.3:deep-linking:form',
'launch_id': self.launch_id,
},
)

@patch.object(DeepLinkingFormView, 'http_response_error')
def test_raises_lti_exception(
self,
http_response_error_mock: MagicMock,
form_class_mock: MagicMock,
validate_deep_linking_message_mock: MagicMock,
get_message_from_cache_mock: MagicMock,
):
Expand All @@ -191,12 +212,14 @@ def test_raises_lti_exception(
)
get_message_from_cache_mock.assert_called_once_with(self.request, self.launch_id)
validate_deep_linking_message_mock.assert_not_called()
form_class_mock.assert_not_called()
http_response_error_mock.assert_called_once_with(exception)

@patch.object(DeepLinkingFormView, 'http_response_error')
def test_raises_deep_linking_exception(
self,
http_response_error_mock: MagicMock,
form_class_mock: MagicMock,
validate_deep_linking_message_mock: MagicMock,
get_message_from_cache_mock: MagicMock,
):
Expand All @@ -210,11 +233,13 @@ def test_raises_deep_linking_exception(
)
get_message_from_cache_mock.assert_called_once_with(self.request, self.launch_id)
validate_deep_linking_message_mock.assert_called_once_with(get_message_from_cache_mock())
form_class_mock.assert_not_called()
http_response_error_mock.assert_called_once_with(exception)


@patch.object(DeepLinkingFormView, 'get_message_from_cache')
@patch(f'{MODULE_PATH}.validate_deep_linking_message')
@patch.object(DeepLinkingFormView, 'form_class')
class TestDeepLinkingFormViewPost(TestCase):
"""Test DeepLinkingFormView post method."""

Expand All @@ -231,6 +256,7 @@ def setUp(self):
def test_post(
self,
http_response_mock: MagicMock,
form_class_mock: MagicMock,
validate_deep_linking_message_mock: MagicMock,
get_message_from_cache_mock: MagicMock,
):
Expand All @@ -239,14 +265,48 @@ def test_post(
self.view_class.as_view()(self.request, self.launch_id),
http_response_mock.return_value,
)
form_class_mock.assert_called_once_with(self.request.POST, request=self.request)
form_class_mock().is_valid.assert_called_once_with()
get_message_from_cache_mock.assert_called_once_with(self.request, self.launch_id)
validate_deep_linking_message_mock.assert_called_once_with(get_message_from_cache_mock())
http_response_mock.assert_called_once_with()
form_class_mock().get_deep_link_resources.assert_called_once_with()
get_message_from_cache_mock().get_deep_link.assert_called_once_with()
get_message_from_cache_mock().get_deep_link().output_response_form.assert_called_once_with(
form_class_mock().get_deep_link_resources(),
)
http_response_mock.assert_called_once_with(
get_message_from_cache_mock().get_deep_link().output_response_form(),
)

@patch.object(DeepLinkingFormView, 'http_response_error')
def test_with_invalid_form(
self,
http_response_error_mock: MagicMock,
form_class_mock: MagicMock,
validate_deep_linking_message_mock: MagicMock,
get_message_from_cache_mock: MagicMock,
):
"""Test with invalid form."""
form_class_mock.return_value.is_valid.return_value = False

self.assertEqual(
self.view_class.as_view()(self.request, self.launch_id),
http_response_error_mock.return_value,
)
form_class_mock.assert_called_once_with(self.request.POST, request=self.request)
form_class_mock().is_valid.assert_called_once_with()
get_message_from_cache_mock.assert_not_called()
validate_deep_linking_message_mock.assert_not_called()
form_class_mock().get_deep_link_resources.assert_not_called()
get_message_from_cache_mock().get_deep_link.assert_not_called()
get_message_from_cache_mock().get_deep_link().output_response_form.assert_not_called()
http_response_error_mock.assert_called_once()

@patch.object(DeepLinkingFormView, 'http_response_error')
def test_raises_lti_exception(
self,
http_response_error_mock: MagicMock,
form_class_mock: MagicMock,
validate_deep_linking_message_mock: MagicMock,
get_message_from_cache_mock: MagicMock,
):
Expand All @@ -258,6 +318,8 @@ def test_raises_lti_exception(
self.view_class.as_view()(self.request, self.launch_id),
http_response_error_mock.return_value,
)
form_class_mock.assert_called_once_with(self.request.POST, request=self.request)
form_class_mock().is_valid.assert_called_once_with()
get_message_from_cache_mock.assert_called_once_with(self.request, self.launch_id)
validate_deep_linking_message_mock.assert_not_called()
http_response_error_mock.assert_called_once_with(exception)
Expand All @@ -266,6 +328,7 @@ def test_raises_lti_exception(
def test_raises_deep_linking_exception(
self,
http_response_error_mock: MagicMock,
form_class_mock: MagicMock,
validate_deep_linking_message_mock: MagicMock,
get_message_from_cache_mock: MagicMock,
):
Expand All @@ -277,6 +340,8 @@ def test_raises_deep_linking_exception(
self.view_class.as_view()(self.request, self.launch_id),
http_response_error_mock.return_value,
)
form_class_mock.assert_called_once_with(self.request.POST, request=self.request)
form_class_mock().is_valid.assert_called_once_with()
get_message_from_cache_mock.assert_called_once_with(self.request, self.launch_id)
validate_deep_linking_message_mock.assert_called_once_with(get_message_from_cache_mock())
http_response_error_mock.assert_called_once_with(exception)
Expand Down
Loading

0 comments on commit 234bf29

Please sign in to comment.