Skip to content

Commit

Permalink
feat: Add CourseAccessConfiguration filter to DeepLinkingForm
Browse files Browse the repository at this point in the history
  • Loading branch information
kuipumu committed May 22, 2024
1 parent 3adfbab commit 3d289c5
Show file tree
Hide file tree
Showing 7 changed files with 380 additions and 75 deletions.
77 changes: 60 additions & 17 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."""
import json
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.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) -> 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()
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
164 changes: 128 additions & 36 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),
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 3d289c5

Please sign in to comment.