Skip to content

Commit

Permalink
Merge pull request #36 from Pearson-Advance/vue/PADV-1201
Browse files Browse the repository at this point in the history
PADV-1201: Filter courses in DeepLinkingForm
  • Loading branch information
kuipumu authored May 23, 2024
2 parents 234bf29 + 11ea567 commit 456a76a
Show file tree
Hide file tree
Showing 7 changed files with 386 additions and 80 deletions.
85 changes: 64 additions & 21 deletions openedx_lti_tool_plugin/deep_linking/forms.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,71 @@
"""Django Forms."""
from typing import List, Optional, Set, Tuple
import json
from typing import 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.contrib.django.lti1p3_tool_config.models import LtiTool
from pylti1p3.deep_link_resource import DeepLinkResource

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.edxapp_wrapper.learning_sequences import course_context
from openedx_lti_tool_plugin.models import CourseAccessConfiguration
from openedx_lti_tool_plugin.waffle import COURSE_ACCESS_CONFIGURATION


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

def __init__(self, *args: tuple, request=None, **kwargs: dict):
content_items = forms.MultipleChoiceField(
required=False,
widget=forms.CheckboxSelectMultiple,
label=_('Courses'),
)

def __init__(
self,
*args: tuple,
request: HttpRequest,
lti_tool: LtiTool,
**kwargs: dict,
):
"""Class __init__ method.
Initialize class instance attributes and add `content_items` field.
Initialize class instance attributes and add field choices
to the `content_items` field.
Args:
*args: Variable length argument list.
request: HttpRequest object.
lti_tool: LtiTool model instance.
**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'),
)
self.request = request
self.lti_tool = lti_tool
self.fields['content_items'].choices = self.get_content_items_choices()

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

def get_content_items_choice(self, course, request: HttpRequest) -> Tuple[str, str]:
def get_content_items_choice(self, course) -> Tuple[str, str]:
"""Get `content_items` field choice.
Args:
course (CourseContext): Course object.
request: HttpRequest object.
Returns:
Tuple containing the choice value and name.
Expand All @@ -68,10 +80,41 @@ def get_content_items_choice(self, course, request: HttpRequest) -> Tuple[str, s
)

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

def get_course_contexts(self):
"""Get CourseContext objects.
Returns:self.cleaned_data
All CourseContext objects if COURSE_ACCESS_CONFIGURATION switch
is disabled or all CourseContext objects matching the IDs in
the CourseAccessConfiguration `allowed_course_ids` field.
Raises:
CourseAccessConfiguration.DoesNotExist: If CourseAccessConfiguration
does not exist for this form `lti_tool` attribute.
"""
if not COURSE_ACCESS_CONFIGURATION.is_enabled():
return course_context().objects.all()

try:
course_access_config = CourseAccessConfiguration.objects.get(
lti_tool=self.lti_tool,
)
except CourseAccessConfiguration.DoesNotExist as exc:
raise DeepLinkingException(
_(f'Course access configuration not found: {self.lti_tool.title}.'),
) from exc

return course_context().objects.filter(
learning_context__context_key__in=json.loads(
course_access_config.allowed_course_ids,
),
)

def get_deep_link_resources(self) -> Set[Optional[DeepLinkResource]]:
"""Get DeepLinkResource objects from this form `cleaned_data` attribute.
Expand Down
166 changes: 129 additions & 37 deletions openedx_lti_tool_plugin/deep_linking/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,80 +1,87 @@
"""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.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.models import CourseAccessConfiguration

MODULE_PATH = f'{MODULE_PATH}.forms'


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

def setUp(self):
"""Set up test fixtures."""
super().setUp()
self.form_class = DeepLinkingForm
self.request = MagicMock()
self.lti_tool = MagicMock()
self.form_kwargs = {'request': self.request, 'lti_tool': self.lti_tool}
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')

@patch.object(DeepLinkingForm, 'get_content_items_choices', return_value=[])
class TestDeepLinkingFormInit(DeepLinkingFormBaseTestCase):
"""Test DeepLinkingForm `__init__` method."""

def test_init(
self,
get_content_items_choices_mock: MagicMock,
gettext_mock: MagicMock,
multiple_choice_field_mock: MagicMock,
):
"""Test `__init__` method."""
form = self.form_class(request=self.request, lti_tool=self.lti_tool)

self.assertEqual(form.request, self.request)
self.assertEqual(form.lti_tool, self.lti_tool)
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(),
list(form.fields['content_items'].choices),
get_content_items_choices_mock.return_value,
)
get_content_items_choices_mock.assert_called_once_with()


@patch.object(DeepLinkingForm, 'get_course_contexts')
@patch.object(DeepLinkingForm, 'get_content_items_choice')
@patch.object(DeepLinkingForm, '__init__', return_value=None)
class TestDeepLinkingFormGetContentItemsChoices(DeepLinkingFormBaseTestCase):
"""Test DeepLinkingForm `get_content_items_choices` method."""

@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
init_mock: MagicMock, # pylint: disable=unused-argument
get_content_items_choice_mock: MagicMock,
course_context_mock: MagicMock,
get_course_contexts_mock: MagicMock,
):
"""Test `get_content_items_choices` method."""
course_context_mock.return_value.objects.all.return_value = [self.course]
get_course_contexts_mock.return_value = [self.course]

self.assertEqual(
self.form_class().get_content_items_choices(self.request),
[get_content_items_choice_mock.return_value],
self.form_class(**self.form_kwargs).get_content_items_choices(),
{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)
get_course_contexts_mock.assert_called_once_with()
get_content_items_choice_mock.assert_called_once_with(self.course)


@patch(f'{MODULE_PATH}.reverse')
@patch.object(DeepLinkingForm, 'get_content_items_choices')
class TestDeepLinkingFormGetContentItemsChoice(DeepLinkingFormBaseTestCase):
"""Test DeepLinkingForm `get_content_items_choice` method."""

@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
get_content_items_choices_mock: 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.form_class(**self.form_kwargs).get_content_items_choice(self.course),
(
self.request.build_absolute_uri.return_value,
self.course.learning_context.title,
Expand All @@ -86,16 +93,101 @@ def test_get_content_items_choice(
)
self.request.build_absolute_uri.assert_called_once_with(reverse_mock())

@patch(f'{MODULE_PATH}.DeepLinkResource')
@patch.object(DeepLinkingForm, '__init__', return_value=None)

@patch(f'{MODULE_PATH}.course_context')
@patch(f'{MODULE_PATH}.json.loads')
@patch.object(CourseAccessConfiguration.objects, 'get')
@patch(f'{MODULE_PATH}.COURSE_ACCESS_CONFIGURATION')
@patch.object(DeepLinkingForm, 'get_content_items_choices')
class TestDeepLinkingFormGetCourseContexts(DeepLinkingFormBaseTestCase):
"""Test DeepLinkingForm `get_course_contexts` method."""

def test_get_course_contexts(
self,
get_content_items_choices_mock: MagicMock, # pylint: disable=unused-argument
course_access_configuration_switch_mock: MagicMock,
course_access_configuration_get_mock: MagicMock,
json_loads_mock: MagicMock,
course_context: MagicMock,
):
"""Test `get_course_contexts` method."""
self.assertEqual(
self.form_class(**self.form_kwargs).get_course_contexts(),
course_context.return_value.objects.filter.return_value,
)
course_access_configuration_switch_mock.is_enabled.assert_called_once_with()
course_access_configuration_get_mock.assert_called_once_with(lti_tool=self.lti_tool)
course_context.assert_called_once_with()
json_loads_mock.assert_called_once_with(
course_access_configuration_get_mock().allowed_course_ids,
)
course_context().objects.filter.assert_called_once_with(
learning_context__context_key__in=json_loads_mock()
)

def test_with_disabled_course_access_configuration_switch(
self,
get_content_items_choices_mock: MagicMock, # pylint: disable=unused-argument
course_access_configuration_switch_mock: MagicMock,
course_access_configuration_get_mock: MagicMock,
json_loads_mock: MagicMock,
course_context: MagicMock,
):
"""Test with disabled `COURSE_ACCESS_CONFIGURATION` switch."""
course_access_configuration_switch_mock.is_enabled.return_value = False

self.assertEqual(
self.form_class(**self.form_kwargs).get_course_contexts(),
course_context.return_value.objects.all.return_value,
)
course_access_configuration_switch_mock.is_enabled.assert_called_once_with()
course_context.assert_called_once_with()
course_context().objects.all.assert_called_once_with()
course_access_configuration_get_mock.assert_not_called()
json_loads_mock.assert_not_called()
course_context().objects.filter.assert_not_called()

@patch(f'{MODULE_PATH}._')
def test_without_course_access_configuration(
self,
gettext_mock: MagicMock,
get_content_items_choices_mock: MagicMock, # pylint: disable=unused-argument
course_access_configuration_switch_mock: MagicMock,
course_access_configuration_get_mock: MagicMock,
json_loads_mock: MagicMock,
course_context: MagicMock,
):
"""Test without CourseAccessConfiguration instance."""
course_access_configuration_get_mock.side_effect = CourseAccessConfiguration.DoesNotExist

with self.assertRaises(DeepLinkingException) as ctxm:
self.form_class(**self.form_kwargs).get_course_contexts()

course_access_configuration_switch_mock.is_enabled.assert_called_once_with()
course_access_configuration_get_mock.assert_called_once_with(lti_tool=self.lti_tool)
gettext_mock.assert_called_once_with(
f'Course access configuration not found: {self.lti_tool.title}.',
)
self.assertEqual(str(gettext_mock()), str(ctxm.exception))
course_context.assert_not_called()
course_context().objects.all.assert_not_called()
json_loads_mock.assert_not_called()
course_context().objects.filter.assert_not_called()


@patch(f'{MODULE_PATH}.DeepLinkResource')
@patch.object(DeepLinkingForm, 'get_content_items_choices')
class TestDeepLinkingFormGetDeepLinkResources(DeepLinkingFormBaseTestCase):
"""Test DeepLinkingForm `get_deep_link_resources` method."""

def test_get_deep_link_resources(
self,
deep_linking_form_init: MagicMock, # pylint: disable=unused-argument
get_content_items_choices_mock: 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 = self.form_class(**self.form_kwargs)
form.cleaned_data = {'content_items': [content_item]}

self.assertEqual(
Expand Down
Loading

0 comments on commit 456a76a

Please sign in to comment.