From 24efb4e8d6255033d80b6907baf8bfb5d0440f01 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 9 Sep 2013 11:50:20 -0400 Subject: [PATCH 01/29] Clean up text of Order Confirmation E-mail. --- lms/djangoapps/shoppingcart/models.py | 9 +++++---- lms/templates/emails/order_confirmation_email.txt | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index d613903057..b3c42a8622 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -382,7 +382,8 @@ class CertificateItem(OrderItem): @property def additional_instruction_text(self): - return textwrap.dedent( - _("Note - you have up to 2 weeks into the course to unenroll from the Verified Certificate option \ - and receive a full refund. To receive your refund, contact {billing_email}.").format( - billing_email=settings.PAYMENT_SUPPORT_EMAIL)) + return _("Note - you have up to 2 weeks into the course to unenroll from the Verified Certificate option " + "and receive a full refund. To receive your refund, contact {billing_email}. " + "Please include your order number in your e-mail. " + "Please do NOT include your credit card information.").format( + billing_email=settings.PAYMENT_SUPPORT_EMAIL) diff --git a/lms/templates/emails/order_confirmation_email.txt b/lms/templates/emails/order_confirmation_email.txt index 7615fd3498..74f945b06a 100644 --- a/lms/templates/emails/order_confirmation_email.txt +++ b/lms/templates/emails/order_confirmation_email.txt @@ -1,7 +1,7 @@ <%! from django.utils.translation import ugettext as _ %> ${_("Hi {name}").format(name=order.user.profile.name)} -${_("Your payment was successful. You will see the charge below on your next credit or debit card statement. The charge will show up on your statement under the company name {platform_name}. If you have billing questions, please read the FAQ or contact {billing_email}. We hope you enjoy your order.").format(platform_name=settings.PLATFORM_NAME,billing_email=settings.PAYMENT_SUPPORT_EMAIL)} +${_("Your payment was successful. You will see the charge below on your next credit or debit card statement. The charge will show up on your statement under the company name {platform_name}. If you have billing questions, please read the FAQ ({faq_url}) or contact {billing_email}.").format(platform_name=settings.PLATFORM_NAME, billing_email=settings.PAYMENT_SUPPORT_EMAIL, faq_url=marketing_link('FAQ'))} ${_("-The {platform_name} Team").format(platform_name=settings.PLATFORM_NAME)} @@ -11,9 +11,9 @@ ${_("The items in your order are:")} ${_("Quantity - Description - Price")} %for order_item in order_items: - ${order_item.qty} - ${order_item.line_desc} - ${order_item.line_cost} + ${order_item.qty} - ${order_item.line_desc} - ${"$" if order_item.currency == 'usd' else ""}${order_item.line_cost} %endfor -${_("Total billed to credit/debit card: {total_cost}").format(total_cost=order.total_cost)} +${_("Total billed to credit/debit card: {currency_symbol}{total_cost}").format(total_cost=order.total_cost, currency_symbol=("$" if order.currency == 'usd' else ""))} %for order_item in order_items: ${order_item.additional_instruction_text} From ee02c06250161dc71f7297db543dd801c210d161 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 19 Aug 2013 17:25:13 +0300 Subject: [PATCH 02/29] Add Learning Tools Interoperability (LTI) blade. LTI blade allows to include LTI components to courses. Python integration, Jasmine and acceptance tests are included. --- CHANGELOG.rst | 4 + .../contentstore/views/component.py | 3 +- cms/djangoapps/contentstore/views/preview.py | 4 +- common/lib/xmodule/setup.py | 1 + common/lib/xmodule/xmodule/course_module.py | 1 + common/lib/xmodule/xmodule/css/lti/lti.scss | 30 +++ .../lib/xmodule/xmodule/js/fixtures/lti.html | 40 +++ .../xmodule/js/spec/lti/constructor.js | 84 ++++++ common/lib/xmodule/xmodule/js/src/lti/lti.js | 26 ++ common/lib/xmodule/xmodule/lti_module.py | 249 ++++++++++++++++++ .../xmodule/modulestore/tests/factories.py | 2 +- common/lib/xmodule/xmodule/tests/__init__.py | 5 +- docs/developers/source/xmodule.rst | 8 + .../courseware/features/lti.feature | 17 ++ lms/djangoapps/courseware/features/lti.py | 188 +++++++++++++ .../courseware/features/lti_setup.py | 50 ++++ .../courseware/mock_lti_server/__init__.py | 0 .../mock_lti_server/mock_lti_server.py | 167 ++++++++++++ .../mock_lti_server/test_mock_lti_server.py | 75 ++++++ lms/djangoapps/courseware/tests/__init__.py | 2 +- lms/djangoapps/courseware/tests/test_lti.py | 79 ++++++ lms/templates/lti.html | 34 +++ 22 files changed, 1062 insertions(+), 7 deletions(-) create mode 100644 common/lib/xmodule/xmodule/css/lti/lti.scss create mode 100644 common/lib/xmodule/xmodule/js/fixtures/lti.html create mode 100644 common/lib/xmodule/xmodule/js/spec/lti/constructor.js create mode 100644 common/lib/xmodule/xmodule/js/src/lti/lti.js create mode 100644 common/lib/xmodule/xmodule/lti_module.py create mode 100644 lms/djangoapps/courseware/features/lti.feature create mode 100644 lms/djangoapps/courseware/features/lti.py create mode 100644 lms/djangoapps/courseware/features/lti_setup.py create mode 100644 lms/djangoapps/courseware/mock_lti_server/__init__.py create mode 100644 lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py create mode 100644 lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py create mode 100644 lms/djangoapps/courseware/tests/test_lti.py create mode 100644 lms/templates/lti.html diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 89f084b3f4..84962963bd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,10 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. + +Blades: Added Learning Tools Interoperability (LTI) blade. Now LTI components +can be included to courses. + LMS: Added alphabetical sorting of forum categories and subcategories. It is hidden behind a false defaulted course level flag. diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 724dc439d9..deef87a403 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -52,7 +52,8 @@ NOTE_COMPONENT_TYPES = ['notes'] ADVANCED_COMPONENT_TYPES = [ 'annotatable', 'word_cloud', - 'graphical_slider_tool' + 'graphical_slider_tool', + 'lti', ] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index ccbb7fb5bb..45ad7a7424 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -81,7 +81,6 @@ def preview_component(request, location): component, 'xmodule_edit.html' ) - return render_to_response('component.html', { 'preview': get_preview_html(request, component, 0), 'editor': component.runtime.render(component, None, 'studio_view').content, @@ -104,7 +103,6 @@ def preview_module_system(request, preview_id, descriptor): return lms_field_data(descriptor._field_data, student_data) course_id = get_course_for_item(descriptor.location).location.course_id - return ModuleSystem( ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'), # TODO (cpennington): Do we want to track how instructors are using the preview problems? @@ -118,6 +116,8 @@ def preview_module_system(request, preview_id, descriptor): xblock_field_data=preview_field_data, can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), mixins=settings.XBLOCK_MIXINS, + course_id=course_id, + anonymous_student_id='student' ) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 704de15ea7..6a24bf8f27 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -56,6 +56,7 @@ setup( "hidden = xmodule.hidden_module:HiddenDescriptor", "raw = xmodule.raw_module:RawDescriptor", "crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor", + "lti = xmodule.lti_module:LTIModuleDescriptor" ], 'console_scripts': [ 'xmodule_assets = xmodule.static_content:main', diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index aca804d5e2..658a095d14 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -153,6 +153,7 @@ class TextbookList(List): class CourseFields(object): + lti_passports = List(help="LTI tools passports as id:client_key:client_secret", scope=Scope.settings) textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course", default=[], scope=Scope.content) wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content) diff --git a/common/lib/xmodule/xmodule/css/lti/lti.scss b/common/lib/xmodule/xmodule/css/lti/lti.scss new file mode 100644 index 0000000000..97a8f62d54 --- /dev/null +++ b/common/lib/xmodule/xmodule/css/lti/lti.scss @@ -0,0 +1,30 @@ +div.lti { + // align center + margin: 0 auto; + + h3.error_message { + display: block; + } + + form.ltiLaunchForm { + display: none; + } + + iframe.ltiLaunchFrame { + width: 100%; + height: 800px; + display: none; + border: 0px; + overflow-x: hidden; + } + + &.rendered { + iframe.ltiLaunchFrame { + display: block; + } + + h3.error_message { + display: none; + } + } +} diff --git a/common/lib/xmodule/xmodule/js/fixtures/lti.html b/common/lib/xmodule/xmodule/js/fixtures/lti.html new file mode 100644 index 0000000000..e5e7ab3f3f --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/lti.html @@ -0,0 +1,40 @@ +
+ +
+ + + + + + + + + + + + + + + + +
+ +

+ Please provide launch_url. Click "Edit", and fill in the + required fields. +

+ + + +
diff --git a/common/lib/xmodule/xmodule/js/spec/lti/constructor.js b/common/lib/xmodule/xmodule/js/spec/lti/constructor.js new file mode 100644 index 0000000000..0a73496bed --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/lti/constructor.js @@ -0,0 +1,84 @@ +/** + * File: constructor.js + * + * Purpose: Jasmine tests for LTI module (front-end part). + * + * + * The front-end part of the LTI module is really simple. If an action + * is set for the hidden LTI form, then it is submited, and the results are + * redirected to an iframe. + * + * We will test that the form is only submited when the action is set (i.e. + * not empty). + * + * Other aspects of LTI module will be covered by Python unit tests and + * acceptance tests. + * + */ + +/* + * "Hence that general is skilful in attack whose opponent does not know what + * to defend; and he is skilful in defense whose opponent does not know what + * to attack." + * + * ~ Sun Tzu + */ + +(function () { + describe('LTI', function () { + describe('constructor', function () { + describe('before settings were filled in', function () { + var element, errorMessage, frame; + + // This function will be executed before each of the it() specs + // in this suite. + beforeEach(function () { + loadFixtures('lti.html'); + + element = $('#lti_id'); + errorMessage = element.find('.error_message'); + form = element.find('.ltiLaunchForm'); + frame = element.find('.ltiLaunchFrame'); + + spyOnEvent(form, 'submit'); + + LTI(element); + }); + + it( + 'when URL setting is filled form is not submited', + function () { + + expect('submit').not.toHaveBeenTriggeredOn(form); + }); + }); + + describe('After the settings were filled in', function () { + var element, errorMessage, frame; + + // This function will be executed before each of the it() specs + // in this suite. + beforeEach(function () { + loadFixtures('lti.html'); + + element = $('#lti_id'); + errorMessage = element.find('.error_message'); + form = element.find('.ltiLaunchForm'); + frame = element.find('.ltiLaunchFrame'); + + spyOnEvent(form, 'submit'); + + // The user "fills in" the necessary settings, and the + // form will get an action URL. + form.attr('action', 'http://www.example.com/'); + + LTI(element); + }); + + it('when URL setting is filled form is submited', function () { + expect('submit').toHaveBeenTriggeredOn(form); + }); + }); + }); + }); +}()); 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..e5b6885e1b --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/lti/lti.js @@ -0,0 +1,26 @@ +window.LTI = (function () { + // Function initialize(element) + // + // Initialize the LTI iframe. + function initialize(element) { + var form; + + // In cms (Studio) the element is already a jQuery object. In lms it is + // a DOM object. + // + // To make sure that there is no error, we pass it through the $() + // function. This will make it a jQuery object if it isn't already so. + element = $(element); + + form = element.find('.ltiLaunchForm'); + + // If the Form's action attribute is set (i.e. we can perform a normal + // submit), then we submit the form and make the frame shown. + if (form.attr('action')) { + form.submit(); + element.find('.lti').addClass('rendered') + } + } + + return initialize; +}()); diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py new file mode 100644 index 0000000000..bc07cea97e --- /dev/null +++ b/common/lib/xmodule/xmodule/lti_module.py @@ -0,0 +1,249 @@ +""" +Module that allows to insert LTI tools to page. + +Module uses current edx-platform 0.14.2 version of requests (oauth part). +Please update code when upgrading requests. + +Protocol is oauth1, LTI version is 1.1.1: +http://www.imsglobal.org/LTI/v1p1p1/ltiIMGv1p1p1.html +""" + +import logging +import requests +import urllib + +from xmodule.editing_module import MetadataOnlyEditingDescriptor +from xmodule.x_module import XModule +from xmodule.course_module import CourseDescriptor +from pkg_resources import resource_string +from xblock.core import String, Scope, List + +log = logging.getLogger(__name__) + + +class LTIError(Exception): + pass + + +class LTIFields(object): + """ + Fields to define and obtain LTI tool from provider are set here, + except credentials, which should be set in course settings:: + + `lti_id` is id to connect tool with credentials in course settings. + `launch_url` is launch url of tool. + `custom_parameters` are additional parameters to navigate to proper book and book page. + + For example, for Vitalsource provider, `launch_url` should be + *https://bc-staging.vitalsource.com/books/book*, + and to get to proper book and book page, you should set custom parameters as:: + + vbid=put_book_id_here + book_location=page/put_page_number_here + + """ + lti_id = String(help="Id of the tool", default='', scope=Scope.settings) + launch_url = String(help="URL of the tool", default='', scope=Scope.settings) + custom_parameters = List(help="Custom parameters (vbid, book_location, etc..)", scope=Scope.settings) + + +class LTIModule(LTIFields, XModule): + ''' + Module provides LTI integration to course. + + Except usual xmodule structure it proceeds with oauth signing. + How it works:: + + 1. Get credentials from course settings. + + 2. There is minimal set of parameters need to be signed (presented for Vitalsource):: + + user_id + oauth_callback + lis_outcome_service_url + lis_result_sourcedid + launch_presentation_return_url + lti_message_type + lti_version + role + *+ all custom parameters* + + These parameters should be encoded and signed by *oauth1* together with + `launch_url` and *POST* request type. + + 3. Signing proceeds with client key/secret pair obtained from course settings. + That pair should be obtained from LTI provider and set into course settings by course author. + After that signature and other oauth data are generated. + + Oauth data which is generated after signing is usual:: + + oauth_callback + oauth_nonce + oauth_consumer_key + oauth_signature_method + oauth_timestamp + oauth_version + + + 4. All that data is passed to form and sent to LTI provider server by browser via + autosubmit via javascript. + + Form example:: + +
+ + + + + + + + + + + + + + + + + + + + +
+ + 5. LTI provider has same secret key and it signs data string via *oauth1* and compares signatures. + + If signatures are correct, LTI provider redirects iframe source to LTI tool web page, + and LTI tool is rendered to iframe inside course. + + 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_html(self): + """ + Renders parameters to template. + """ + + # Obtains client_key and client_secret credentials from current course: + course_id = self.runtime.course_id + course_location = CourseDescriptor.id_to_location(course_id) + course = self.descriptor.runtime.modulestore.get_item(course_location) + client_key = client_secret = '' + for lti_passport in course.lti_passports: + try: + lti_id, key, secret = lti_passport.split(':') + except ValueError: + raise LTIError('Could not parse LTI passport: {0!r}. \ + Should be "id:key:secret" string.'.format(lti_passport)) + if lti_id == self.lti_id: + client_key, client_secret = key, secret + break + + # parsing custom parameters to dict + custom_parameters = {} + for custom_parameter in self.custom_parameters: + try: + param_name, param_value = custom_parameter.split('=', 1) + except ValueError: + raise LTIError('Could not parse custom parameter: {0!r}. \ + Should be "x=y" string.'.format(custom_parameter)) + + # LTI specs: 'custom_' should be prepended before each custom parameter + custom_parameters[u'custom_' + unicode(param_name)] = unicode(param_value) + + input_fields = self.oauth_params( + custom_parameters, + client_key, + client_secret + ) + + context = { + 'input_fields': input_fields, + + # these params do not participate in oauth signing + 'launch_url': self.launch_url, + 'element_id': self.location.html_id(), + 'element_class': self.location.category, + } + + return self.system.render_template('lti.html', context) + + def oauth_params(self, custom_parameters, client_key, client_secret): + """ + Signs request and returns signature and oauth parameters. + + `custom_paramters` is dict of parsed `custom_parameter` field + + `client_key` and `client_secret` are LTI tool credentials. + + Also *anonymous student id* is passed to template and therefore to LTI provider. + """ + + client = requests.auth.Client( + client_key=unicode(client_key), + client_secret=unicode(client_secret) + ) + + user_id = self.runtime.anonymous_student_id + assert user_id is not None + + # must have parameters for correct signing from LTI: + body = { + u'user_id': user_id, + u'oauth_callback': u'about:blank', + u'lis_outcome_service_url': '', + u'lis_result_sourcedid': '', + u'launch_presentation_return_url': '', + u'lti_message_type': u'basic-lti-launch-request', + u'lti_version': 'LTI-1p0', + u'role': u'student' + } + + # appending custom parameter for signing + body.update(custom_parameters) + + # This is needed for body encoding: + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + + __, headers, __ = client.sign( + unicode(self.launch_url), + http_method=u'POST', + body=body, + headers=headers) + params = headers['Authorization'] + # parse headers to pass to template as part of context: + params = dict([param.strip().replace('"', '').split('=') for param in params.split(',')]) + + params[u'oauth_nonce'] = params[u'OAuth oauth_nonce'] + del params[u'OAuth oauth_nonce'] + + # 0.14.2 (current) version of requests oauth library encodes signature, + # with 'Content-Type': 'application/x-www-form-urlencoded' + # so '='' becomes '%3D'. + # We send form via browser, so browser will encode it again, + # So we need to decode signature back: + params[u'oauth_signature'] = urllib.unquote(params[u'oauth_signature']).decode('utf8') + + # add lti parameters to oauth parameters for sending in form + params.update(body) + return params + + +class LTIModuleDescriptor(LTIFields, MetadataOnlyEditingDescriptor): + """ + LTIModuleDescriptor provides no export/import to xml. + """ + module_class = LTIModule diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 4ad801aef8..c8228c5e3e 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -27,7 +27,7 @@ class XModuleCourseFactory(Factory): store = editable_modulestore('direct') # Write the data to the mongo datastore - new_course = store.create_xmodule(location) + new_course = store.create_xmodule(location, metadata=kwargs.get('metadata', None)) # This metadata code was copied from cms/djangoapps/contentstore/views.py if display_name is not None: diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index fefa668a56..b7e5ea8435 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -40,7 +40,7 @@ open_ended_grading_interface = { } -def get_test_system(): +def get_test_system(course_id=''): """ Construct a test ModuleSystem instance. @@ -66,7 +66,8 @@ def get_test_system(): node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), xblock_field_data=lambda descriptor: descriptor._field_data, anonymous_student_id='student', - open_ended_grading_interface=open_ended_grading_interface + open_ended_grading_interface=open_ended_grading_interface, + course_id=course_id, ) diff --git a/docs/developers/source/xmodule.rst b/docs/developers/source/xmodule.rst index 008a1303d2..77ee2ea684 100644 --- a/docs/developers/source/xmodule.rst +++ b/docs/developers/source/xmodule.rst @@ -95,6 +95,14 @@ Html :members: :show-inheritance: + +LTI +=== + +.. automodule:: xmodule.lti_module + :members: + :show-inheritance: + Mako ==== diff --git a/lms/djangoapps/courseware/features/lti.feature b/lms/djangoapps/courseware/features/lti.feature new file mode 100644 index 0000000000..abdcfdb704 --- /dev/null +++ b/lms/djangoapps/courseware/features/lti.feature @@ -0,0 +1,17 @@ +Feature: LTI component + As a student, I want to view LTI component in LMS. + + Scenario: LTI component in LMS is not rendered + Given the course has correct LTI credentials + And the course has an LTI component with incorrect fields + Then I view the LTI and it is not rendered + + Scenario: LTI component in LMS is rendered + Given the course has correct LTI credentials + And the course has an LTI component filled with correct fields + Then I view the LTI and it is rendered + + Scenario: LTI component in LMS is rendered incorrectly + Given the course has incorrect LTI credentials + And the course has an LTI component filled with correct fields + Then I view the LTI but incorrect_signature warning is rendered \ No newline at end of file diff --git a/lms/djangoapps/courseware/features/lti.py b/lms/djangoapps/courseware/features/lti.py new file mode 100644 index 0000000000..0e91d5ed02 --- /dev/null +++ b/lms/djangoapps/courseware/features/lti.py @@ -0,0 +1,188 @@ +#pylint: disable=C0111 + +from django.contrib.auth.models import User +from lettuce import world, step +from lettuce.django import django_url +from common import course_id + +from student.models import CourseEnrollment + + +@step('I view the LTI and it is not rendered$') +def lti_is_not_rendered(_step): + # lti div has no class rendered + assert world.is_css_not_present('div.lti.rendered') + + # error is shown + assert world.css_visible('.error_message') + + # iframe is not visible + assert not world.css_visible('iframe') + + #inside iframe test content is not presented + with world.browser.get_iframe('ltiLaunchFrame') as iframe: + # iframe does not contain functions from terrain/ui_helpers.py + assert iframe.is_element_not_present_by_css('.result', wait_time=5) + + +@step('I view the LTI and it is rendered$') +def lti_is_rendered(_step): + # lti div has class rendered + assert world.is_css_present('div.lti.rendered') + + # error is hidden + assert not world.css_visible('.error_message') + + # iframe is visible + assert world.css_visible('iframe') + + #inside iframe test content is presented + with world.browser.get_iframe('ltiLaunchFrame') as iframe: + # iframe does not contain functions from terrain/ui_helpers.py + assert iframe.is_element_present_by_css('.result', wait_time=5) + assert ("This is LTI tool. Success." == world.retry_on_exception( + lambda: iframe.find_by_css('.result')[0].text, + max_attempts=5 + )) + + +@step('I view the LTI but incorrect_signature warning is rendered$') +def incorrect_lti_is_rendered(_step): + # lti div has class rendered + assert world.is_css_present('div.lti.rendered') + + # error is hidden + assert not world.css_visible('.error_message') + + # iframe is visible + assert world.css_visible('iframe') + + #inside iframe test content is presented + with world.browser.get_iframe('ltiLaunchFrame') as iframe: + # iframe does not contain functions from terrain/ui_helpers.py + assert iframe.is_element_present_by_css('.result', wait_time=5) + assert ("Wrong LTI signature" == world.retry_on_exception( + lambda: iframe.find_by_css('.result')[0].text, + max_attempts=5 + )) + + +@step('the course has correct LTI credentials$') +def set_correct_lti_passport(_step): + coursenum = 'test_course' + metadata = { + 'lti_passports': ["correct_lti_id:{}:{}".format( + world.lti_server.oauth_settings['client_key'], + world.lti_server.oauth_settings['client_secret'] + )] + } + i_am_registered_for_the_course(coursenum, metadata) + + +@step('the course has incorrect LTI credentials$') +def set_incorrect_lti_passport(_step): + coursenum = 'test_course' + metadata = { + 'lti_passports': ["test_lti_id:{}:{}".format( + world.lti_server.oauth_settings['client_key'], + "incorrect_lti_secret_key" + )] + } + i_am_registered_for_the_course(coursenum, metadata) + + +@step('the course has an LTI component filled with correct fields$') +def add_correct_lti_to_course(_step): + category = 'lti' + world.ItemFactory.create( + # parent_location=section_location(course), + parent_location=world.scenario_dict['SEQUENTIAL'].location, + category=category, + display_name='LTI', + metadata={ + 'lti_id': 'correct_lti_id', + 'launch_url': world.lti_server.oauth_settings['lti_base'] + world.lti_server.oauth_settings['lti_endpoint'] + } + ) + course = world.scenario_dict["COURSE"] + chapter_name = world.scenario_dict['SECTION'].display_name.replace( + " ", "_") + section_name = chapter_name + path = "/courses/{org}/{num}/{name}/courseware/{chapter}/{section}".format( + org=course.org, + num=course.number, + name=course.display_name.replace(' ', '_'), + chapter=chapter_name, + section=section_name) + url = django_url(path) + + world.browser.visit(url) + + +@step('the course has an LTI component with incorrect fields$') +def add_incorrect_lti_to_course(_step): + category = 'lti' + world.ItemFactory.create( + parent_location=world.scenario_dict['SEQUENTIAL'].location, + category=category, + display_name='LTI', + metadata={ + 'lti_id': 'incorrect_lti_id', + 'lti_url': world.lti_server.oauth_settings['lti_base'] + world.lti_server.oauth_settings['lti_endpoint'] + } + ) + course = world.scenario_dict["COURSE"] + chapter_name = world.scenario_dict['SECTION'].display_name.replace( + " ", "_") + section_name = chapter_name + path = "/courses/{org}/{num}/{name}/courseware/{chapter}/{section}".format( + org=course.org, + num=course.number, + name=course.display_name.replace(' ', '_'), + chapter=chapter_name, + section=section_name) + url = django_url(path) + + world.browser.visit(url) + + +def create_course(course, metadata): + + # First clear the modulestore so we don't try to recreate + # the same course twice + # This also ensures that the necessary templates are loaded + world.clear_courses() + + # Create the course + # We always use the same org and display name, + # but vary the course identifier (e.g. 600x or 191x) + world.scenario_dict['COURSE'] = world.CourseFactory.create( + org='edx', + number=course, + display_name='Test Course', + metadata=metadata + ) + + # Add a section to the course to contain problems + world.scenario_dict['SECTION'] = world.ItemFactory.create( + parent_location=world.scenario_dict['COURSE'].location, + display_name='Test Section' + ) + world.scenario_dict['SEQUENTIAL'] = world.ItemFactory.create( + parent_location=world.scenario_dict['SECTION'].location, + category='sequential', + display_name='Test Section') + + +def i_am_registered_for_the_course(course, metadata): + # Create the course + create_course(course, metadata) + + # Create the user + world.create_user('robot', 'test') + usr = User.objects.get(username='robot') + + # If the user is not already enrolled, enroll the user. + CourseEnrollment.enroll(usr, course_id(course)) + + world.log_in(username='robot', password='test') diff --git a/lms/djangoapps/courseware/features/lti_setup.py b/lms/djangoapps/courseware/features/lti_setup.py new file mode 100644 index 0000000000..0a6c4590dd --- /dev/null +++ b/lms/djangoapps/courseware/features/lti_setup.py @@ -0,0 +1,50 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from courseware.mock_lti_server.mock_lti_server import MockLTIServer +from lettuce import before, after, world +from django.conf import settings +import threading + +from logging import getLogger +logger = getLogger(__name__) + + +@before.all +def setup_mock_lti_server(): + + server_host = '127.0.0.1' + + # Add +1 to XQUEUE random port number + server_port = settings.XQUEUE_PORT + 1 + + address = (server_host, server_port) + + # Create the mock server instance + server = MockLTIServer(address) + logger.debug("LTI server started at {} port".format(str(server_port))) + # Start the server running in a separate daemon thread + # Because the thread is a daemon, it will terminate + # when the main thread terminates. + server_thread = threading.Thread(target=server.serve_forever) + server_thread.daemon = True + server_thread.start() + + server.oauth_settings = { + 'client_key': 'test_client_key', + 'client_secret': 'test_client_secret', + 'lti_base': 'http://{}:{}/'.format(server_host, server_port), + 'lti_endpoint': 'correct_lti_endpoint' + } + + # Store the server instance in lettuce's world + # so that other steps can access it + # (and we can shut it down later) + world.lti_server = server + + +@after.all +def teardown_mock_lti_server(total): + + # Stop the LTI server and free up the port + world.lti_server.shutdown() diff --git a/lms/djangoapps/courseware/mock_lti_server/__init__.py b/lms/djangoapps/courseware/mock_lti_server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py b/lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py new file mode 100644 index 0000000000..ba9cea84d6 --- /dev/null +++ b/lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py @@ -0,0 +1,167 @@ +from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler +import urlparse +from requests.packages.oauthlib.oauth1.rfc5849 import signature +import mock +from logging import getLogger +logger = getLogger(__name__) + + +class MockLTIRequestHandler(BaseHTTPRequestHandler): + ''' + A handler for LTI POST requests. + ''' + + protocol = "HTTP/1.0" + + def do_HEAD(self): + self._send_head() + + def do_POST(self): + ''' + Handle a POST request from the client and sends response back. + ''' + self._send_head() + + post_dict = self._post_dict() # Retrieve the POST data + + logger.debug("LTI provider received POST request {} to path {}".format( + str(post_dict), + self.path) + ) # Log the request + + # Respond only to requests with correct lti endpoint: + if self._is_correct_lti_request(): + correct_keys = [ + 'user_id', + 'role', + 'oauth_nonce', + 'oauth_timestamp', + 'oauth_consumer_key', + 'lti_version', + 'oauth_signature_method', + 'oauth_version', + 'oauth_signature', + 'lti_message_type', + 'oauth_callback', + 'lis_outcome_service_url', + 'lis_result_sourcedid', + 'launch_presentation_return_url' + ] + + if sorted(correct_keys) != sorted(post_dict.keys()): + status_message = "Incorrect LTI header" + else: + params = {k: v for k, v in post_dict.items() if k != 'oauth_signature'} + if self.server.check_oauth_signature(params, post_dict['oauth_signature']): + status_message = "This is LTI tool. Success." + else: + status_message = "Wrong LTI signature" + else: + status_message = "Invalid request URL" + + self._send_response(status_message) + + def _send_head(self): + ''' + Send the response code and MIME headers + ''' + if self._is_correct_lti_request(): + self.send_response(200) + else: + self.send_response(500) + + self.send_header('Content-type', 'text/html') + self.end_headers() + + def _post_dict(self): + ''' + Retrieve the POST parameters from the client as a dictionary + ''' + try: + length = int(self.headers.getheader('content-length')) + post_dict = urlparse.parse_qs(self.rfile.read(length), keep_blank_values=True) + # The POST dict will contain a list of values for each key. + # None of our parameters are lists, however, so we map [val] --> val. + # If the list contains multiple entries, we pick the first one + post_dict = {key: val[0] for key, val in post_dict.items()} + except: + # We return an empty dict here, on the assumption + # that when we later check that the request has + # the correct fields, it won't find them, + # and will therefore send an error response + return {} + return post_dict + + def _send_response(self, message): + ''' + Send message back to the client + ''' + response_str = """TEST TITLE + +

IFrame loaded

\ +

Server response is:

\ +

{}

+ """.format(message) + + # Log the response + logger.debug("LTI: sent response {}".format(response_str)) + + self.wfile.write(response_str) + + def _is_correct_lti_request(self): + '''If url to LTI tool is correct.''' + return self.server.oauth_settings['lti_endpoint'] in self.path + + +class MockLTIServer(HTTPServer): + ''' + A mock LTI provider server that responds + to POST requests to localhost. + ''' + + def __init__(self, address): + ''' + Initialize the mock XQueue server instance. + + *address* is the (host, host's port to listen to) tuple. + ''' + handler = MockLTIRequestHandler + HTTPServer.__init__(self, address, handler) + + def shutdown(self): + ''' + Stop the server and free up the port + ''' + # First call superclass shutdown() + HTTPServer.shutdown(self) + # We also need to manually close the socket + self.socket.close() + + def check_oauth_signature(self, params, client_signature): + ''' + Checks oauth signature from client. + + `params` are params from post request except signature, + `client_signature` is signature from request. + + Builds mocked request and verifies hmac-sha1 signing:: + 1. builds string to sign from `params`, `url` and `http_method`. + 2. signs it with `client_secret` which comes from server settings. + 3. obtains signature after sign and then compares it with request.signature + (request signature comes form client in request) + + Returns `True` if signatures are correct, otherwise `False`. + + ''' + client_secret = unicode(self.oauth_settings['client_secret']) + url = self.oauth_settings['lti_base'] + self.oauth_settings['lti_endpoint'] + + request = mock.Mock() + + request.params = [(unicode(k), unicode(v)) for k, v in params.items()] + request.uri = unicode(url) + request.http_method = u'POST' + request.signature = unicode(client_signature) + + return signature.verify_hmac_sha1(request, client_secret) + diff --git a/lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py b/lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py new file mode 100644 index 0000000000..99650d5faa --- /dev/null +++ b/lms/djangoapps/courseware/mock_lti_server/test_mock_lti_server.py @@ -0,0 +1,75 @@ +""" +Test for Mock_LTI_Server +""" +import unittest +import threading +import urllib +from mock_lti_server import MockLTIServer + +from nose.plugins.skip import SkipTest + + +class MockLTIServerTest(unittest.TestCase): + ''' + A mock version of the LTI provider server that listens on a local + port and responds with pre-defined grade messages. + + Used for lettuce BDD tests in lms/courseware/features/lti.feature + ''' + + def setUp(self): + + # This is a test of the test setup, + # so it does not need to run as part of the unit test suite + # You can re-enable it by commenting out the line below + # raise SkipTest + + # Create the server + server_port = 8034 + server_host = '127.0.0.1' + address = (server_host, server_port) + self.server = MockLTIServer(address) + self.server.oauth_settings = { + 'client_key': 'test_client_key', + 'client_secret': 'test_client_secret', + 'lti_base': 'http://{}:{}/'.format(server_host, server_port), + 'lti_endpoint': 'correct_lti_endpoint' + } + # Start the server in a separate daemon thread + server_thread = threading.Thread(target=self.server.serve_forever) + server_thread.daemon = True + server_thread.start() + + def tearDown(self): + + # Stop the server, freeing up the port + self.server.shutdown() + + def test_request(self): + """ + Tests that LTI server processes request with right program + path, and responses with incorrect signature. + """ + request = { + 'user_id': 'default_user_id', + 'role': 'student', + 'oauth_nonce': '', + 'oauth_timestamp': '', + 'oauth_consumer_key': 'client_key', + 'lti_version': 'LTI-1p0', + 'oauth_signature_method': 'HMAC-SHA1', + 'oauth_version': '1.0', + 'oauth_signature': '', + 'lti_message_type': 'basic-lti-launch-request', + 'oauth_callback': 'about:blank', + 'launch_presentation_return_url': '', + 'lis_outcome_service_url': '', + 'lis_result_sourcedid': '' + } + + response_handle = urllib.urlopen( + self.server.oauth_settings['lti_base'] + self.server.oauth_settings['lti_endpoint'], + urllib.urlencode(request) + ) + response = response_handle.read() + self.assertTrue('Wrong LTI signature' in response) diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index 88129cc8d1..0a4d3508b8 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -86,7 +86,7 @@ class BaseTestXmodule(ModuleStoreTestCase): data=self.DATA ) - self.runtime = get_test_system() + self.runtime = get_test_system(course_id=self.course.id) # Allow us to assert that the template was called in the same way from # different code paths while maintaining the type returned by render_template self.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items())) diff --git a/lms/djangoapps/courseware/tests/test_lti.py b/lms/djangoapps/courseware/tests/test_lti.py new file mode 100644 index 0000000000..d2b4ea6867 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_lti.py @@ -0,0 +1,79 @@ +"""LTI integration tests""" + +import requests +from . import BaseTestXmodule +from collections import OrderedDict +import mock + + +class TestLTI(BaseTestXmodule): + """ + Integration test for lti xmodule. + + It checks overall code, by assuring that context that goes to template is correct. + As part of that, checks oauth signature generation by mocking signing function of `requests` library. + """ + CATEGORY = "lti" + + def setUp(self): + """ + Mock oauth1 signing of requests library for testing. + """ + super(TestLTI, self).setUp() + mocked_nonce = u'135685044251684026041377608307' + mocked_timestamp = u'1234567890' + mocked_signature_after_sign = u'my_signature%3D' + mocked_decoded_signature = u'my_signature=' + + self.correct_headers = { + u'oauth_callback': u'about:blank', + u'lis_outcome_service_url': '', + u'lis_result_sourcedid': '', + u'launch_presentation_return_url': '', + u'lti_message_type': u'basic-lti-launch-request', + u'lti_version': 'LTI-1p0', + + u'oauth_nonce': mocked_nonce, + u'oauth_timestamp': mocked_timestamp, + u'oauth_consumer_key': u'', + u'oauth_signature_method': u'HMAC-SHA1', + u'oauth_version': u'1.0', + u'user_id': self.runtime.anonymous_student_id, + u'role': u'student', + u'oauth_signature': mocked_decoded_signature + } + + saved_sign = requests.auth.Client.sign + + def mocked_sign(self, *args, **kwargs): + """ + Mocked oauth1 sign function. + """ + # self is here: + __, headers, __ = saved_sign(self, *args, **kwargs) + # we should replace nonce, timestamp and signed_signature in headers: + old = headers[u'Authorization'] + old_parsed = OrderedDict([param.strip().replace('"', '').split('=') for param in old.split(',')]) + old_parsed[u'OAuth oauth_nonce'] = mocked_nonce + old_parsed[u'oauth_timestamp'] = mocked_timestamp + old_parsed[u'oauth_signature'] = mocked_signature_after_sign + headers[u'Authorization'] = ', '.join([k+'="'+v+'"' for k, v in old_parsed.items()]) + return None, headers, None + + patcher = mock.patch.object(requests.auth.Client, "sign", mocked_sign) + patcher.start() + self.addCleanup(patcher.stop) + + def test_lti_constructor(self): + """ + Makes sure that all parameters extracted. + """ + self.runtime.render_template = lambda template, context: context + generated_context = self.item_module.get_html() + expected_context = { + 'input_fields': self.correct_headers, + 'element_class': self.item_module.location.category, + 'element_id': self.item_module.location.html_id(), + 'launch_url': '', # default value + } + self.assertDictEqual(generated_context, expected_context) diff --git a/lms/templates/lti.html b/lms/templates/lti.html new file mode 100644 index 0000000000..3d97c8d808 --- /dev/null +++ b/lms/templates/lti.html @@ -0,0 +1,34 @@ +
+ + ## This form will be hidden. Once available on the client, the LTI + ## module JavaScript will trigget a "submit" on the form, and the + ## result will be rendered to the below iFrame. +
+ + % for param_name, param_value in input_fields.items(): + + %endfor + + +
+ +

+ Please provide launch_url. Click "Edit", and fill in the + required fields. +

+ + ## The result of the form submit will be rendered here. + + +
From 5c7443be38f3b21a13af4731f92757a8af951a32 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Mon, 9 Sep 2013 16:14:52 -0400 Subject: [PATCH 03/29] cleanup copy on reqs, photo, and choose pages --- common/templates/course_modes/choose.html | 2 +- lms/static/sass/views/_verification.scss | 8 +++++++- lms/templates/verify_student/photo_verification.html | 6 ++++-- lms/templates/verify_student/show_requirements.html | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html index dfaf4e98e7..2b943ed4bc 100644 --- a/common/templates/course_modes/choose.html +++ b/common/templates/course_modes/choose.html @@ -128,7 +128,7 @@ $(document).ready(function() {

${_("What is an ID Verified Certificate?")}

-

${_("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim.")}

+

${_("An ID Verified Certificate requires proof of your identity through your photo and ID and is checked throughout the course to verify that it is you who earned the passing grade.")}

% endif diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index ec974b194e..adfb9aaa60 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -680,6 +680,7 @@ // help - general list .list-help { margin-top: ($baseline/2); + color: $black; .help-item { margin-bottom: ($baseline/4); @@ -865,6 +866,7 @@ } .help-tips { + margin-left: $baseline; .title { @extend .hd-lv5; @@ -876,6 +878,7 @@ // help - general list .list-tips { + color: $black; .tip { margin-bottom: ($baseline/4); @@ -1496,7 +1499,7 @@ border-color: $m-pink-l3; .title { - @extend .t-title4; + @extend .t-title5; @extend .t-weight4; border-bottom-color: $m-pink-l3; background: tint($m-pink, 95%); @@ -1615,6 +1618,9 @@ // VIEW: review photos &.step-review { + .modal.edit-name .submit input { + color: #fff; + } .nav-wizard { diff --git a/lms/templates/verify_student/photo_verification.html b/lms/templates/verify_student/photo_verification.html index 71373e93ad..a54ab6a2b0 100644 --- a/lms/templates/verify_student/photo_verification.html +++ b/lms/templates/verify_student/photo_verification.html @@ -79,7 +79,7 @@
-

${_("Don't see your picture? Make sure to allow your browser to use your camera when it asks for permission.
Still not working? You can {a_start} audit the course {a_end} without verifying.").format(a_start='', a_end="")}

+

${_("Don't see your picture? Make sure to allow your browser to use your camera when it asks for permission.")}


@@ -133,6 +133,8 @@
${_("What do you do with this picture?")}
${_("We only use it to verify your identity. It is not displayed anywhere.")}
+
${_("What if my camera isn't working?")}
+
${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='', a_end="")}
@@ -164,7 +166,7 @@
-

${_("Don't see your picture? Make sure to allow your browser to use your camera when it asks for permission. Still not working? You can {a_start} audit the course {a_end} without verifying.").format(a_start='', a_end="")}

+

${_("Don't see your picture? Make sure to allow your browser to use your camera when it asks for permission.")}


diff --git a/lms/templates/verify_student/show_requirements.html b/lms/templates/verify_student/show_requirements.html index 4f21ce3020..a3510d7f9e 100644 --- a/lms/templates/verify_student/show_requirements.html +++ b/lms/templates/verify_student/show_requirements.html @@ -87,7 +87,7 @@

- ${_("Check Your Email")} + ${_("Check your email")} ${_("you need an active edX account before registering - check your email for instructions")}

From 18b4b0b03f4260ac53860564ea138dc2dfc4abe6 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Mon, 9 Sep 2013 19:24:58 -0400 Subject: [PATCH 04/29] modal functionality for edit name on vcerts --- lms/static/js/verify_student/photocapture.js | 4 +- lms/static/sass/views/_verification.scss | 17 +++++++ .../verify_student/_modal_editname.html | 50 +++++++++++-------- .../verify_student/photo_verification.html | 27 +++++----- 4 files changed, 63 insertions(+), 35 deletions(-) diff --git a/lms/static/js/verify_student/photocapture.js b/lms/static/js/verify_student/photocapture.js index a214cf06c3..2401fde2c1 100644 --- a/lms/static/js/verify_student/photocapture.js +++ b/lms/static/js/verify_student/photocapture.js @@ -70,6 +70,8 @@ function doSnapshotButton(captureButton, resetButton, approveButton) { function submitNameChange(event) { event.preventDefault(); + $("#lean_overlay").fadeOut(200); + $("#edit-name").css({ 'display' : 'none' }); var full_name = $('input[name="name"]').val(); var xhr = $.post( "/change_name", @@ -84,7 +86,7 @@ function submitNameChange(event) { .fail(function(jqXhr,text_status, error_thrown) { $('.message-copy').html(jqXhr.responseText); }); - + } function initSnapshotHandler(names, hasHtml5CameraSupport) { diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index adfb9aaa60..3622410652 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -1622,6 +1622,23 @@ color: #fff; } + .modal { + + fieldset { + margin-top: $baseline; + } + + .close-modal { + color: $m-blue-d3; + + &:hover { + color: $m-blue-d1; + border: none; + } + } + + } + .nav-wizard { .help-inline { diff --git a/lms/templates/verify_student/_modal_editname.html b/lms/templates/verify_student/_modal_editname.html index dbe8551854..1e5efadc44 100644 --- a/lms/templates/verify_student/_modal_editname.html +++ b/lms/templates/verify_student/_modal_editname.html @@ -1,26 +1,34 @@ <%! from django.utils.translation import ugettext as _ %> diff --git a/lms/templates/verify_student/photo_verification.html b/lms/templates/verify_student/photo_verification.html index a54ab6a2b0..8118d87e45 100644 --- a/lms/templates/verify_student/photo_verification.html +++ b/lms/templates/verify_student/photo_verification.html @@ -249,19 +249,6 @@
    -
  1. -

    ${_("Check Your Name")}

    - -
    -

    ${_("Make sure your full name on your edX account ({full_name}) matches your ID. We will also use this as the name on your certificate.").format(full_name="" + user_full_name + "")}

    -
    - - -
  2. ${_("Review the Photos You've Taken")}

    @@ -315,6 +302,20 @@
+
  • +

    ${_("Check Your Name")}

    + +
    +

    ${_("Make sure your full name on your edX account ({full_name}) matches your ID. We will also use this as the name on your certificate.").format(full_name="" + user_full_name + "")}

    +
    + + +
  • +
  • ${_("Check Your Contribution Level")}

    From d066692bcef664505c8d70ba573e3d507254f9a2 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 10 Sep 2013 10:59:31 -0400 Subject: [PATCH 05/29] Verification: revising textarea reset formatting and increasing close modal control size --- lms/static/sass/views/_verification.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index 3622410652..65b9e5f2b7 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -218,10 +218,15 @@ // reset: forms - input { + input,textarea { font-style: normal; font-weight: 400; margin-right: ($baseline/5); + padding: ($baseline/4) ($baseline/2); + } + + textarea { + padding: ($baseline/2); } label { @@ -1629,6 +1634,7 @@ } .close-modal { + @include font-size(24); color: $m-blue-d3; &:hover { From 740e5bace71937058bb4458f245efef5cc173031 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 10 Sep 2013 11:24:35 -0400 Subject: [PATCH 06/29] Prevent being able to click on disabled button in IE. Enable display of course org, num in receipt page. --- common/djangoapps/course_modes/views.py | 5 +++- lms/djangoapps/shoppingcart/models.py | 12 ++++++++++ lms/djangoapps/shoppingcart/views.py | 1 + lms/djangoapps/verify_student/views.py | 17 +++++++++++--- lms/static/js/verify_student/photocapture.js | 23 +++++++++++-------- lms/static/sass/elements/_controls.scss | 3 +++ .../shoppingcart/verified_cert_receipt.html | 12 ++++------ .../verify_student/_verification_header.html | 2 +- .../verify_student/photo_verification.html | 8 +++---- 9 files changed, 56 insertions(+), 27 deletions(-) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 641185eb5b..5d60b03859 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -25,10 +25,13 @@ class ChooseModeView(View): if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) modes = CourseMode.modes_for_course_dict(course_id) + course = course_from_id(course_id) context = { "course_id": course_id, "modes": modes, - "course_name": course_from_id(course_id).display_name, + "course_name": course.display_name_with_default, + "course_org" : course.display_org_with_default, + "course_num" : course.display_number_with_default, "chosen_price": None, "error": error, } diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index b3c42a8622..3bc2a03802 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -380,6 +380,18 @@ class CertificateItem(OrderItem): else: return super(CertificateItem, self).single_item_receipt_template + @property + def single_item_receipt_context(self): + course = course_from_id(self.course_id) + return { + "course_id" : self.course_id, + "course_name": course.display_name_with_default, + "course_org": course.display_org_with_default, + "course_num": course.display_number_with_default, + "course_start_date_text": course.start_date_text, + "course_has_started": course.start > datetime.today().replace(tzinfo=pytz.utc), + } + @property def additional_instruction_text(self): return _("Note - you have up to 2 weeks into the course to unenroll from the Verified Certificate option " diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index fff8b22e08..8930136b80 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -113,5 +113,6 @@ def show_receipt(request, ordernum): if order_items.count() == 1: receipt_template = order_items[0].single_item_receipt_template + context.update(order_items[0].single_item_receipt_context) return render_to_response(receipt_template, context) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 85e7cb5309..db0e1f3407 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -55,11 +55,15 @@ class VerifyView(View): chosen_price = request.session["donation_for_course"][course_id] else: chosen_price = verify_mode.min_price + + course = course_from_id(course_id) context = { "progress_state": progress_state, "user_full_name": request.user.profile.name, "course_id": course_id, - "course_name": course_from_id(course_id).display_name, + "course_name": course.display_name_with_default, + "course_org" : course.display_org_with_default, + "course_num" : course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), "suggested_prices": [ decimal.Decimal(price) @@ -91,9 +95,12 @@ class VerifiedView(View): else: chosen_price = verify_mode.min_price.format("{:g}") + course = course_from_id(course_id) context = { "course_id": course_id, - "course_name": course_from_id(course_id).display_name, + "course_name": course.display_name_with_default, + "course_org" : course.display_org_with_default, + "course_num" : course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), "currency": verify_mode.currency.upper(), "chosen_price": chosen_price, @@ -150,10 +157,14 @@ def show_requirements(request, course_id): """ if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) + + course = course_from_id(course_id) context = { "course_id": course_id, + "course_name": course.display_name_with_default, + "course_org" : course.display_org_with_default, + "course_num" : course.display_number_with_default, "is_not_active": not request.user.is_active, - "course_name": course_from_id(course_id).display_name, } return render_to_response("verify_student/show_requirements.html", context) diff --git a/lms/static/js/verify_student/photocapture.js b/lms/static/js/verify_student/photocapture.js index a214cf06c3..45a926d055 100644 --- a/lms/static/js/verify_student/photocapture.js +++ b/lms/static/js/verify_student/photocapture.js @@ -47,18 +47,20 @@ var submitToPaymentProcessing = function() { }); } -function doResetButton(resetButton, captureButton, approveButton, nextButton) { +function doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink) { approveButton.removeClass('approved'); - nextButton.addClass('disabled'); + nextButtonNav.addClass('is-not-ready'); + nextLink.attr('href', "#"); captureButton.show(); resetButton.hide(); approveButton.hide(); } -function doApproveButton(approveButton, nextButton) { +function doApproveButton(approveButton, nextButtonNav, nextLink) { + nextButtonNav.removeClass('is-not-ready'); approveButton.addClass('approved'); - nextButton.removeClass('disabled'); + nextLink.attr('href', "#next"); } function doSnapshotButton(captureButton, resetButton, approveButton) { @@ -67,7 +69,6 @@ function doSnapshotButton(captureButton, resetButton, approveButton) { approveButton.show(); } - function submitNameChange(event) { event.preventDefault(); var full_name = $('input[name="name"]').val(); @@ -84,7 +85,7 @@ function submitNameChange(event) { .fail(function(jqXhr,text_status, error_thrown) { $('.message-copy').html(jqXhr.responseText); }); - + } function initSnapshotHandler(names, hasHtml5CameraSupport) { @@ -99,7 +100,8 @@ function initSnapshotHandler(names, hasHtml5CameraSupport) { var captureButton = $("#" + name + "_capture_button"); var resetButton = $("#" + name + "_reset_button"); var approveButton = $("#" + name + "_approve_button"); - var nextButton = $("#" + name + "_next_button"); + var nextButtonNav = $("#" + name + "_next_button_nav"); + var nextLink = $("#" + name + "_next_link"); var flashCapture = $("#" + name + "_flash"); var ctx = null; @@ -137,12 +139,12 @@ function initSnapshotHandler(names, hasHtml5CameraSupport) { flashCapture[0].reset(); } - doResetButton(resetButton, captureButton, approveButton, nextButton); + doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink); return false; } function approve() { - doApproveButton(approveButton, nextButton) + doApproveButton(approveButton, nextButtonNav, nextLink) return false; } @@ -150,7 +152,8 @@ function initSnapshotHandler(names, hasHtml5CameraSupport) { captureButton.show(); resetButton.hide(); approveButton.hide(); - nextButton.addClass('disabled'); + nextButtonNav.addClass('is-not-ready'); + nextLink.attr('href', "#"); // Connect event handlers... video.click(snapshot); diff --git a/lms/static/sass/elements/_controls.scss b/lms/static/sass/elements/_controls.scss index e7d884d146..b179c04b9b 100644 --- a/lms/static/sass/elements/_controls.scss +++ b/lms/static/sass/elements/_controls.scss @@ -176,6 +176,9 @@ cursor: default; pointer-events: none; box-shadow: none; + :hover { + pointer-events: none; + } } // ==================== diff --git a/lms/templates/shoppingcart/verified_cert_receipt.html b/lms/templates/shoppingcart/verified_cert_receipt.html index 063e32e173..d2f00942a1 100644 --- a/lms/templates/shoppingcart/verified_cert_receipt.html +++ b/lms/templates/shoppingcart/verified_cert_receipt.html @@ -1,8 +1,6 @@ <%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> <%! from student.views import course_from_id %> -<%! from datetime import datetime %> -<%! import pytz %> <%inherit file="../main.html" /> <%block name="bodyclass">register verification-process step-confirmation @@ -15,8 +13,6 @@ ${notification} % endif -<% course_id = order_items[0].course_id %> -<% course = course_from_id(course_id) %>
    @@ -25,7 +21,7 @@

    ${_("You are now registered for")} - ${course.display_name} + ${course_name} (${course_org}, ${course_num}) @@ -108,11 +104,11 @@ ${item.line_desc} - ${_("Starts: {start_date}").format(start_date=course.start_date_text)} + ${_("Starts: {start_date}").format(start_date=course_start_date_text)} - %if course.start > datetime.today().replace(tzinfo=pytz.utc): - ${_("Starts: {start_date}").format(start_date=course.start_date_text)} + %if course_has_started: + ${_("Starts: {start_date}").format(start_date=course_start_date_text)} %else: ${_("Go to Course")} %endif diff --git a/lms/templates/verify_student/_verification_header.html b/lms/templates/verify_student/_verification_header.html index 171d92dfee..8e5957fa33 100644 --- a/lms/templates/verify_student/_verification_header.html +++ b/lms/templates/verify_student/_verification_header.html @@ -4,7 +4,7 @@

    ${_("You are registering for")} - ${course_name} + ${course_name} (${course_org}, ${course_num}) diff --git a/lms/templates/verify_student/photo_verification.html b/lms/templates/verify_student/photo_verification.html index a54ab6a2b0..40dd8436a2 100644 --- a/lms/templates/verify_student/photo_verification.html +++ b/lms/templates/verify_student/photo_verification.html @@ -141,12 +141,12 @@

  • -