From b797c6bd0962c0fffc1bc7fd1e3a8bbb5809fd1e Mon Sep 17 00:00:00 2001 From: Paul Medlock-Walton Date: Wed, 17 Sep 2014 14:18:09 -0400 Subject: [PATCH] username and email request for lti module allow username and email can be passed to a lti third party app ask user permission when lti button is clicked allow course editor to customize text on lti launch button --- AUTHORS | 2 + common/lib/xmodule/xmodule/js/src/lti/lti.js | 32 ++++++++ common/lib/xmodule/xmodule/lti_module.py | 77 +++++++++++++++++++ .../courseware/features/lti.feature | 70 +++++++++++++++++ lms/djangoapps/courseware/features/lti.py | 45 +++++++++-- .../courseware/tests/test_lti_integration.py | 4 + lms/templates/lti.html | 9 ++- 7 files changed, 231 insertions(+), 8 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/lti/lti.js diff --git a/AUTHORS b/AUTHORS index c170b8459f..9df68c1aa9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -173,3 +173,5 @@ Jason Zhu Marceau Cnudde Braden MacDonald Jonathan Piacenti +Paul Medlock-Walton +Henry Tareque diff --git a/common/lib/xmodule/xmodule/js/src/lti/lti.js b/common/lib/xmodule/xmodule/js/src/lti/lti.js new file mode 100644 index 0000000000..a1212671ab --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/lti/lti.js @@ -0,0 +1,32 @@ +(function () { + 'use strict'; + /** + * This function will process all the attributes from the DOM element passed, taking all of + * the configuration attributes. It uses the request-username and request-email + * to prompt the user to decide if they want to share their personal information + * with the third party application connecting through LTI. + * @constructor + * @param {jQuery} element DOM element with the lti container. + */ + this.LTI = function (element) { + var dataAttrs = $(element).find('.lti').data(), + askToSendUsername = (dataAttrs.askToSendUsername === 'True'), + askToSendEmail = (dataAttrs.askToSendEmail === 'True'); + + // When the lti button is clicked, provide users the option to + // accept or reject sending their information to a third party + $(element).on('click', '.link_lti_new_window', function () { + if(askToSendUsername && askToSendEmail) { + return confirm(gettext("Click OK to have your username and e-mail address sent to a 3rd party application.\n\nClick Cancel to return to this page without sending your information.")); + } else if (askToSendUsername) { + return confirm(gettext("Click OK to have your username sent to a 3rd party application.\n\nClick Cancel to return to this page without sending your information.")); + } else if (askToSendEmail) { + return confirm(gettext("Click OK to have your e-mail address sent to a 3rd party application.\n\nClick Cancel to return to this page without sending your information.")); + } else { + return true; + } + }); + + }; + +}).call(this); diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py index c5e8458f0f..1da01cd9d4 100644 --- a/common/lib/xmodule/xmodule/lti_module.py +++ b/common/lib/xmodule/xmodule/lti_module.py @@ -192,6 +192,48 @@ class LTIFields(object): scope=Scope.settings ) + # Users will be presented with a message indicating that their e-mail/username would be sent to a third + # party application. When "Open in New Page" is not selected, the tool automatically appears without any user action. + ask_to_send_username = Boolean( + display_name=_("Request user's username"), + # Translators: This is used to request the user's username for a third party service. + # Usernames can only be requested if "Open in New Page" is set to True. + help=_( + "Select True to request the user's username. You must also set Open in New Page to True to get the user's information." + ), + default=False, + scope=Scope.settings + ) + ask_to_send_email = Boolean( + display_name=_("Request user's email"), + # Translators: This is used to request the user's email for a third party service. + # Emails can only be requested if "Open in New Page" is set to True. + help=_( + "Select True to request the user's email address. You must also set Open in New Page to True to get the user's information." + ), + default=False, + scope=Scope.settings + ) + + description = String( + display_name=_("LTI Application Information"), + help=_( + "Enter a description of the third party application. If requesting username and/or email, use this text box to inform users " + "why their username and/or email will be forwarded to a third party application." + ), + default="", + scope=Scope.settings + ) + + button_text = String( + display_name=_("Button Text"), + help=_( + "Enter the text on the button used to launch the third party application." + ), + default="", + scope=Scope.settings + ) + class LTIModule(LTIFields, LTI20ModuleMixin, XModule): """ @@ -274,7 +316,13 @@ class LTIModule(LTIFields, LTI20ModuleMixin, XModule): Otherwise error message from LTI provider is generated. """ + js = { + 'js': [ + resource_string(__name__, 'js/src/lti/lti.js') + ] + } css = {'scss': [resource_string(__name__, 'css/lti/lti.scss')]} + js_module_name = "LTI" def get_input_fields(self): # LTI provides a list of default parameters that might be passed as @@ -317,6 +365,7 @@ class LTIModule(LTIFields, LTI20ModuleMixin, XModule): # parsing custom parameters to dict custom_parameters = {} + for custom_parameter in self.custom_parameters: try: param_name, param_value = [p.strip() for p in custom_parameter.split('=', 1)] @@ -370,6 +419,11 @@ class LTIModule(LTIFields, LTI20ModuleMixin, XModule): 'weight': self.weight, 'module_score': self.module_score, 'comment': sanitized_comment, + 'description': self.description, + 'ask_to_send_username': self.ask_to_send_username, + 'ask_to_send_email': self.ask_to_send_email, + 'button_text': self.button_text, + } def get_html(self): @@ -516,6 +570,29 @@ class LTIModule(LTIFields, LTI20ModuleMixin, XModule): u'lis_outcome_service_url': self.get_outcome_service_url() }) + self.user_email = "" + self.user_username = "" + + # Username and email can't be sent in studio mode, because the user object is not defined. + # To test functionality test in LMS + + if callable(self.runtime.get_real_user): + real_user_object = self.runtime.get_real_user(self.runtime.anonymous_student_id) + try: + self.user_email = real_user_object.email + except AttributeError: + self.user_email = "" + try: + self.user_username = real_user_object.username + except AttributeError: + self.user_username = "" + + if self.open_in_a_new_page: + if self.ask_to_send_username and self.user_username: + body["lis_person_sourcedid"] = self.user_username + if self.ask_to_send_email and self.user_email: + body["lis_person_contact_email_primary"] = self.user_email + # Appending custom parameter for signing. body.update(custom_parameters) diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature index c14ac8a8d3..6beac25ac3 100644 --- a/lms/djangoapps/courseware/features/lti.feature +++ b/lms/djangoapps/courseware/features/lti.feature @@ -137,3 +137,73 @@ Feature: LMS.LTI component | True | True | Then in the LTI component I do not see an provider iframe Then I see LTI component module title with text "LTI (EXTERNAL RESOURCE)" + + #13 + Scenario: LTI component button text is correctly displayed + Given the course has correct LTI credentials with registered Instructor + And the course has an LTI component with correct fields: + | button_text | + | Launch Application | + Then I see LTI component button with text "Launch Application" + + #14 + Scenario: LTI component description is correctly displayed + Given the course has correct LTI credentials with registered Instructor + And the course has an LTI component with correct fields: + | description | + | Application description | + Then I see LTI component description with text "Application description" + + #15 + Scenario: LTI component requests permission for username and is rejected + Given the course has correct LTI credentials with registered Instructor + And the course has an LTI component with correct fields: + | ask_to_send_username | + | True | + Then I view the permission alert + Then I reject the permission alert and do not view the LTI + + #16 + Scenario: LTI component requests permission for username and displays LTI when accepted + Given the course has correct LTI credentials with registered Instructor + And the course has an LTI component with correct fields: + | ask_to_send_username | + | True | + Then I view the permission alert + Then I accept the permission alert and view the LTI + + #17 + Scenario: LTI component requests permission for email and is rejected + Given the course has correct LTI credentials with registered Instructor + And the course has an LTI component with correct fields: + | ask_to_send_email | + | True | + Then I view the permission alert + Then I reject the permission alert and do not view the LTI + + #18 + Scenario: LTI component requests permission for email and displays LTI when accepted + Given the course has correct LTI credentials with registered Instructor + And the course has an LTI component with correct fields: + | ask_to_send_email | + | True | + Then I view the permission alert + Then I accept the permission alert and view the LTI + + #19 + Scenario: LTI component requests permission for email and username and is rejected + Given the course has correct LTI credentials with registered Instructor + And the course has an LTI component with correct fields: + | ask_to_send_email | ask_to_send_username | + | True | True | + Then I view the permission alert + Then I reject the permission alert and do not view the LTI + + #20 + Scenario: LTI component requests permission for email and username and displays LTI when accepted + Given the course has correct LTI credentials with registered Instructor + And the course has an LTI component with correct fields: + | ask_to_send_email | ask_to_send_username | + | True | True | + Then I view the permission alert + Then I accept the permission alert and view the LTI diff --git a/lms/djangoapps/courseware/features/lti.py b/lms/djangoapps/courseware/features/lti.py index f82bd2f702..a365749a61 100644 --- a/lms/djangoapps/courseware/features/lti.py +++ b/lms/djangoapps/courseware/features/lti.py @@ -56,11 +56,39 @@ def lti_is_rendered(_step, rendered_in): assert not world.is_css_present('iframe', wait_time=2) assert world.is_css_present('.link_lti_new_window', wait_time=0) assert not world.is_css_present('.error_message', wait_time=0) - check_lti_popup() + click_and_check_lti_popup() else: # incorrent rendered_in parameter assert False +@step('I view the permission alert$') +def view_lti_permission_alert(_step): + assert not world.is_css_present('iframe', wait_time=2) + assert world.is_css_present('.link_lti_new_window', wait_time=0) + assert not world.is_css_present('.error_message', wait_time=0) + world.css_find('.link_lti_new_window').first.click() + alert = world.browser.get_alert() + assert alert is not None + assert len(world.browser.windows) == 1 + + +@step('I accept the permission alert and view the LTI$') +def accept_lti_permission_alert(_step): + parent_window = world.browser.current_window # Save the parent window + assert len(world.browser.windows) == 1 + alert = world.browser.get_alert() + alert.accept() + assert len(world.browser.windows) != 1 + check_lti_popup(parent_window) + + +@step('I reject the permission alert and do not view the LTI$') +def reject_lti_permission_alert(_step): + alert = world.browser.get_alert() + alert.dismiss() + assert len(world.browser.windows) == 1 + + @step('I view the LTI but incorrect_signature warning is rendered$') def incorrect_lti_is_rendered(_step): assert world.is_css_present('iframe', wait_time=2) @@ -216,10 +244,7 @@ def i_am_registered_for_the_course(coursenum, metadata, user='Instructor'): world.log_in(username=user.username, password='test') -def check_lti_popup(): - parent_window = world.browser.current_window # Save the parent window - world.css_find('.link_lti_new_window').first.click() - +def check_lti_popup(parent_window): assert len(world.browser.windows) != 1 for window in world.browser.windows: @@ -239,6 +264,12 @@ def check_lti_popup(): world.browser.switch_to_window(parent_window) # Switch to the main window again +def click_and_check_lti_popup(): + parent_window = world.browser.current_window # Save the parent window + world.css_find('.link_lti_new_window').first.click() + check_lti_popup(parent_window) + + @step('visit the LTI component') def visit_lti_component(_step): visit_scenario_item('LTI') @@ -249,7 +280,9 @@ def see_elem_text(_step, elem, text): selector_map = { 'progress': '.problem-progress', 'feedback': '.problem-feedback', - 'module title': '.problem-header' + 'module title': '.problem-header', + 'button': '.link_lti_new_window', + 'description': '.lti-description' } assert_in(elem, selector_map) assert_true(world.css_has_text(selector_map[elem], text)) diff --git a/lms/djangoapps/courseware/tests/test_lti_integration.py b/lms/djangoapps/courseware/tests/test_lti_integration.py index 943d06f0dd..43b673cfee 100644 --- a/lms/djangoapps/courseware/tests/test_lti_integration.py +++ b/lms/djangoapps/courseware/tests/test_lti_integration.py @@ -89,6 +89,10 @@ class TestLTI(BaseTestXmodule): 'module_score': None, 'comment': u'', 'weight': 1.0, + 'ask_to_send_username': self.item_descriptor.ask_to_send_username, + 'ask_to_send_email': self.item_descriptor.ask_to_send_email, + 'description': self.item_descriptor.description, + 'button_text': self.item_descriptor.button_text, } def mocked_sign(self, *args, **kwargs): diff --git a/lms/templates/lti.html b/lms/templates/lti.html index 91348a8a5e..75de15d5d1 100644 --- a/lms/templates/lti.html +++ b/lms/templates/lti.html @@ -21,13 +21,18 @@
% if launch_url and launch_url != 'http://www.example.com' and not hide_launch: % if open_in_a_new_page: @@ -43,7 +48,7 @@

${_('Please provide launch_url. Click "Edit", and fill in the required fields.')}

-%endif +% endif % if has_score and comment: