286 lines
9.7 KiB
Python
286 lines
9.7 KiB
Python
"""
|
|
Code related to LTI course tab functionality.
|
|
"""
|
|
from typing import Dict
|
|
from urllib.parse import quote
|
|
|
|
from django.contrib.auth.models import AbstractBaseUser
|
|
from django.contrib.sites.shortcuts import get_current_site
|
|
from django.http import HttpRequest
|
|
from django.utils.translation import get_language, gettext_lazy, to_locale
|
|
from lti_consumer.api import get_lti_pii_sharing_state_for_course
|
|
from lti_consumer.lti_1p1.contrib.django import lti_embed
|
|
from lti_consumer.models import LtiConfiguration
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from web_fragments.fragment import Fragment
|
|
|
|
from lms.djangoapps.courseware.access import get_user_role
|
|
from lms.djangoapps.courseware.tabs import EnrolledTab
|
|
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
|
|
from openedx.core.djangolib.markup import HTML
|
|
from common.djangoapps.student.models import anonymous_id_for_user
|
|
from xmodule.course_block import CourseBlock # lint-amnesty, pylint: disable=wrong-import-order
|
|
from xmodule.tabs import TabFragmentViewMixin, key_checker # lint-amnesty, pylint: disable=wrong-import-order
|
|
|
|
|
|
class LtiCourseLaunchMixin:
|
|
"""
|
|
Mixin that encapsulates all LTI-related functionality from the View
|
|
"""
|
|
|
|
ROLE_MAP = {
|
|
'student': 'Student,Learner',
|
|
'staff': 'Administrator',
|
|
'instructor': 'Instructor',
|
|
}
|
|
DEFAULT_ROLE = 'Student,Learner'
|
|
|
|
def _get_pii_lti_parameters(self, course: CourseBlock, request: HttpRequest) -> Dict[str, str]:
|
|
"""
|
|
Get LTI parameters that contain PII.
|
|
|
|
Args:
|
|
course (CourseBlock): CourseBlock object.
|
|
request (HttpRequest): Request object for view in which LTI will be embedded.
|
|
|
|
Returns:
|
|
Dictionary with LTI parameters containing PII.
|
|
"""
|
|
pii_sharing_allowed = get_lti_pii_sharing_state_for_course(course.id)
|
|
if not pii_sharing_allowed:
|
|
return {}
|
|
lti_config = self._get_lti_config(course)
|
|
# Currently only LTI 1.1 is supported by the tab
|
|
if lti_config.version != lti_config.LTI_1P1:
|
|
return {}
|
|
|
|
pii_config = {}
|
|
if lti_config.pii_share_username:
|
|
pii_config['person_sourcedid'] = request.user.username
|
|
|
|
if lti_config.pii_share_email:
|
|
pii_config['person_contact_email_primary'] = request.user.email
|
|
return pii_config
|
|
|
|
def _get_additional_lti_parameters(self, course: CourseBlock, request: HttpRequest) -> Dict[str, str]:
|
|
"""
|
|
Get additional misc LTI parameters.
|
|
|
|
Args:
|
|
course (CourseBlock): CourseBlock object.
|
|
request (HttpRequest): Request object for view in which LTI will be embedded.
|
|
|
|
Returns:
|
|
Dictionary with additional LTI parameters.
|
|
"""
|
|
lti_config = self._get_lti_config(course)
|
|
additional_config = lti_config.lti_config.get('additional_parameters', {})
|
|
return additional_config
|
|
|
|
@staticmethod
|
|
def _get_user_id(user: AbstractBaseUser, course_key: CourseKey):
|
|
return anonymous_id_for_user(user, course_key)
|
|
|
|
def _get_lti_roles(self, user: AbstractBaseUser, course_key: CourseKey) -> str:
|
|
return self.ROLE_MAP.get(
|
|
get_user_role(user, course_key),
|
|
self.DEFAULT_ROLE,
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_context_id(course_key: CourseKey) -> str:
|
|
return quote(str(course_key))
|
|
|
|
@staticmethod
|
|
def _get_resource_link_id(course_key: CourseKey, request: HttpRequest) -> str:
|
|
site = get_current_site(request)
|
|
return '{}-{}'.format(
|
|
site.domain,
|
|
str(course_key.make_usage_key('course', course_key.run)),
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_result_sourcedid(context_id: str, resource_link_id: str, user_id: str) -> str:
|
|
return "{context}:{resource_link}:{user_id}".format(
|
|
context=context_id,
|
|
resource_link=resource_link_id,
|
|
user_id=user_id,
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_context_title(course: CourseBlock) -> str:
|
|
return "{} - {}".format(
|
|
course.display_name_with_default,
|
|
course.display_org_with_default,
|
|
)
|
|
|
|
def _get_lti_config(self, course: CourseBlock) -> LtiConfiguration:
|
|
raise NotImplementedError
|
|
|
|
def _get_lti_embed_code(self, course: CourseBlock, request: HttpRequest) -> str:
|
|
"""
|
|
Returns the LTI embed code for embedding in the current course context.
|
|
Args:
|
|
course (CourseBlock): CourseBlock object.
|
|
request (HttpRequest): Request object for view in which LTI will be embedded.
|
|
Returns:
|
|
HTML code to embed LTI in course page.
|
|
"""
|
|
course_key = course.id
|
|
lti_config = self._get_lti_config(course)
|
|
lti_consumer = lti_config.get_lti_consumer()
|
|
user_id = quote(self._get_user_id(request.user, course_key))
|
|
context_id = quote(self._get_context_id(course_key))
|
|
resource_link_id = quote(self._get_resource_link_id(course_key, request))
|
|
roles = self._get_lti_roles(request.user, course_key)
|
|
context_title = self._get_context_title(course)
|
|
result_sourcedid = quote(self._get_result_sourcedid(context_id, resource_link_id, user_id))
|
|
additional_params = self._get_additional_lti_parameters(course, request)
|
|
pii_params = self._get_pii_lti_parameters(course, request)
|
|
locale = to_locale(get_language())
|
|
|
|
return lti_embed(
|
|
html_element_id='lti-tab-launcher',
|
|
lti_consumer=lti_consumer,
|
|
resource_link_id=resource_link_id,
|
|
user_id=user_id,
|
|
roles=roles,
|
|
context_id=context_id,
|
|
context_title=context_title,
|
|
context_label=context_id,
|
|
result_sourcedid=result_sourcedid,
|
|
launch_presentation_locale=locale,
|
|
**pii_params,
|
|
**additional_params,
|
|
)
|
|
|
|
# pylint: disable=unused-argument
|
|
def render_to_fragment(self, request: HttpRequest, course: CourseBlock, **kwargs) -> Fragment:
|
|
"""
|
|
Returns a fragment view for the LTI launch.
|
|
Args:
|
|
request (HttpRequest): request object
|
|
course (CourseBlock): A course object
|
|
Returns:
|
|
A Fragment that embeds LTI in a course page.
|
|
"""
|
|
lti_embed_html = self._get_lti_embed_code(course, request)
|
|
|
|
fragment = Fragment(
|
|
HTML(
|
|
"""
|
|
<iframe
|
|
id='lti-tab-embed'
|
|
srcdoc='{srcdoc}'
|
|
>
|
|
</iframe>
|
|
"""
|
|
).format(
|
|
srcdoc=lti_embed_html
|
|
)
|
|
)
|
|
fragment.add_css(
|
|
"""
|
|
#lti-tab-embed {
|
|
width: 100%;
|
|
min-height: 800px;
|
|
border: none;
|
|
}
|
|
"""
|
|
)
|
|
return fragment
|
|
|
|
|
|
class LtiCourseTab(LtiCourseLaunchMixin, EnrolledTab):
|
|
"""
|
|
A tab to add custom LTI components to a course in a tab.
|
|
"""
|
|
type = 'lti_tab'
|
|
priority = 120
|
|
is_default = False
|
|
allow_multiple = True
|
|
|
|
def _get_lti_config(self, course: CourseBlock) -> LtiConfiguration:
|
|
return LtiConfiguration.objects.get(config_id=self.lti_config_id)
|
|
|
|
def __init__(self, tab_dict=None, name=None, lti_config_id=None):
|
|
def link_func(course, reverse_func):
|
|
""" Returns a function that returns the lti tab's URL. """
|
|
return reverse_func('lti_course_tab', args=[str(course.id), self.lti_config_id])
|
|
|
|
self.lti_config_id = tab_dict.get('lti_config_id') if tab_dict else lti_config_id
|
|
|
|
if tab_dict is None:
|
|
tab_dict = {}
|
|
|
|
if name is not None:
|
|
tab_dict['name'] = name
|
|
|
|
tab_dict['link_func'] = link_func
|
|
tab_dict['tab_id'] = f'lti_tab_{self.lti_config_id}'
|
|
|
|
super().__init__(tab_dict)
|
|
|
|
@classmethod
|
|
def validate(cls, tab_dict, raise_error=True):
|
|
"""
|
|
Ensures that the specified tab_dict is valid.
|
|
"""
|
|
return (
|
|
super().validate(tab_dict, raise_error)
|
|
and key_checker(['name', 'lti_config_id'])(tab_dict, raise_error)
|
|
)
|
|
|
|
def __getitem__(self, key):
|
|
if key == 'lti_config_id':
|
|
return self.lti_config_id
|
|
else:
|
|
return super().__getitem__(key)
|
|
|
|
def __setitem__(self, key, value):
|
|
if key == 'lti_config_id':
|
|
self.lti_config_id = value
|
|
else:
|
|
super().__setitem__(key, value)
|
|
|
|
def to_json(self):
|
|
"""
|
|
Return a dictionary representation of this tab.
|
|
"""
|
|
to_json_val = super().to_json()
|
|
to_json_val.update({'lti_config_id': self.lti_config_id})
|
|
return to_json_val
|
|
|
|
def __eq__(self, other):
|
|
if not super().__eq__(other):
|
|
return False
|
|
return self.lti_config_id == other.get('lti_config_id')
|
|
|
|
def __hash__(self):
|
|
"""
|
|
Return a hash representation of Tab Object.
|
|
"""
|
|
return hash(repr(self))
|
|
|
|
|
|
class DiscussionLtiCourseTab(LtiCourseLaunchMixin, TabFragmentViewMixin, EnrolledTab):
|
|
"""
|
|
Course tab that loads the associated LTI-based discussion provider in a tab.
|
|
"""
|
|
type = 'lti_discussion'
|
|
priority = 41
|
|
allow_multiple = False
|
|
is_dynamic = True
|
|
title = gettext_lazy("Discussion")
|
|
|
|
def _get_lti_config(self, course: CourseBlock) -> LtiConfiguration:
|
|
config = DiscussionsConfiguration.get(course.id)
|
|
return config.lti_configuration
|
|
|
|
@classmethod
|
|
def is_enabled(cls, course, user=None):
|
|
"""Check if the tab is enabled."""
|
|
if super().is_enabled(course, user):
|
|
return DiscussionsConfiguration.lti_discussion_enabled(course.id)
|
|
return False
|