From fe1e659fb7d7d02460e1f2d6550a82093848c069 Mon Sep 17 00:00:00 2001 From: Matt Drayer Date: Fri, 13 May 2016 23:44:54 -0400 Subject: [PATCH 1/8] mattdrayer/rc/2016-05-17: Check for "code" on receipt line item attribute. --- lms/static/js/commerce/views/receipt_view.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lms/static/js/commerce/views/receipt_view.js b/lms/static/js/commerce/views/receipt_view.js index c3c1fe7a71..9f8bc6b2ef 100644 --- a/lms/static/js/commerce/views/receipt_view.js +++ b/lms/static/js/commerce/views/receipt_view.js @@ -247,7 +247,13 @@ var edx = edx || {}; for (var i = 0; i < length; i++) { var line = order.lines[i], attributeValues = _.find(line.product.attribute_values, function (attribute) { - return attribute.name === 'course_key' + // If the attribute has a 'code' property, compare its value, otherwise compare 'name' + var value_to_match = 'course_key'; + if (attribute.code) { + return attribute.code === value_to_match; + } else { + return attribute.name === value_to_match; + } }); // This method assumes that all items in the order are related to a single course. From defb08e8453457df59df0a3f5c07a2fd5254e3ba Mon Sep 17 00:00:00 2001 From: Douglas Hall Date: Wed, 11 May 2016 16:21:29 -0400 Subject: [PATCH 2/8] Include course price in course_about template context if either the ecommerce service is enabled or shoppingcart is enabled --- lms/djangoapps/courseware/views/views.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 60aaf4f34d..15dd151f66 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -510,7 +510,8 @@ def course_about(request, course_id): ecommerce_bulk_checkout_link = '' professional_mode = None ecomm_service = EcommerceService() - if ecomm_service.is_enabled(request.user) and ( + _is_ecomm_service_enabled = ecomm_service.is_enabled(request.user) + if _is_ecomm_service_enabled and ( CourseMode.PROFESSIONAL in modes or CourseMode.NO_ID_PROFESSIONAL_MODE in modes ): professional_mode = modes.get(CourseMode.PROFESSIONAL, '') or \ @@ -519,11 +520,14 @@ def course_about(request, course_id): if professional_mode.bulk_sku: ecommerce_bulk_checkout_link = ecomm_service.checkout_page_url(professional_mode.bulk_sku) - # Find the minimum price for the course across all course modes - registration_price = CourseMode.min_course_price_for_currency( - course_key, - settings.PAID_COURSE_REGISTRATION_CURRENCY[0] - ) + # We need to look up the price from the CourseMode only when the EcommerceService is enabled OR + # the legacy shoppingcart ecommerce implementation is enabled. + registration_price = 0 + if _is_ecomm_service_enabled or _is_shopping_cart_enabled: + registration_price = CourseMode.min_course_price_for_currency( + course_key, + settings.PAID_COURSE_REGISTRATION_CURRENCY[0] + ) course_price = get_cosmetic_display_price(course, registration_price) can_add_course_to_cart = _is_shopping_cart_enabled and registration_price From 4bdeaf76962ed00a2b12ddeb5186bdb454fac688 Mon Sep 17 00:00:00 2001 From: Douglas Hall Date: Wed, 11 May 2016 21:26:56 -0400 Subject: [PATCH 3/8] Always include the course price in the course_about template context --- lms/djangoapps/courseware/views/views.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 15dd151f66..9cdeb1d62f 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -510,24 +510,19 @@ def course_about(request, course_id): ecommerce_bulk_checkout_link = '' professional_mode = None ecomm_service = EcommerceService() - _is_ecomm_service_enabled = ecomm_service.is_enabled(request.user) - if _is_ecomm_service_enabled and ( - CourseMode.PROFESSIONAL in modes or CourseMode.NO_ID_PROFESSIONAL_MODE in modes - ): + is_professional_mode = CourseMode.PROFESSIONAL in modes or CourseMode.NO_ID_PROFESSIONAL_MODE in modes + if ecomm_service.is_enabled(request.user) and (is_professional_mode): professional_mode = modes.get(CourseMode.PROFESSIONAL, '') or \ modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE, '') ecommerce_checkout_link = ecomm_service.checkout_page_url(professional_mode.sku) if professional_mode.bulk_sku: ecommerce_bulk_checkout_link = ecomm_service.checkout_page_url(professional_mode.bulk_sku) - # We need to look up the price from the CourseMode only when the EcommerceService is enabled OR - # the legacy shoppingcart ecommerce implementation is enabled. - registration_price = 0 - if _is_ecomm_service_enabled or _is_shopping_cart_enabled: - registration_price = CourseMode.min_course_price_for_currency( - course_key, - settings.PAID_COURSE_REGISTRATION_CURRENCY[0] - ) + # Find the minimum price for the course across all course modes + registration_price = CourseMode.min_course_price_for_currency( + course_key, + settings.PAID_COURSE_REGISTRATION_CURRENCY[0] + ) course_price = get_cosmetic_display_price(course, registration_price) can_add_course_to_cart = _is_shopping_cart_enabled and registration_price From cafc15cf7da3e160d020d859451ec951dae28102 Mon Sep 17 00:00:00 2001 From: cahrens Date: Mon, 16 May 2016 10:27:36 -0400 Subject: [PATCH 4/8] Move city and country fields to the very end. OSPR-1155 --- lms/djangoapps/instructor/tests/test_api.py | 5 +++++ lms/djangoapps/instructor/views/api.py | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 7adc6e3256..7a3391b008 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -2525,6 +2525,9 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment Test that some minimum of information is formatted correctly in the response to get_students_features. """ + for student in self.students: + student.profile.city = "Mos Eisley {}".format(student.id) + student.profile.save() url = reverse('get_students_features', kwargs={'course_id': self.course.id.to_deprecated_string()}) response = self.client.get(url, {}) res_json = json.loads(response.content) @@ -2536,6 +2539,8 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment ][0] self.assertEqual(student_json['username'], student.username) self.assertEqual(student_json['email'], student.email) + self.assertEqual(student_json['city'], student.profile.city) + self.assertEqual(student_json['country'], "") @ddt.data(True, False) def test_get_students_features_cohorted(self, is_cohorted): diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 26369ecbb1..a21db446bc 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1245,7 +1245,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red query_features = [ 'id', 'username', 'name', 'email', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', - 'goals', 'city', 'country' + 'goals', ] # Provide human-friendly and translatable names for these features. These names @@ -1263,8 +1263,6 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red 'level_of_education': _('Level of Education'), 'mailing_address': _('Mailing Address'), 'goals': _('Goals'), - 'city': _('City'), - 'country': _('Country'), } if is_course_cohorted(course.id): @@ -1276,6 +1274,12 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red query_features.append('team') query_features_names['team'] = _('Team') + # For compatibility reasons, city and country should always appear last. + query_features.append('city') + query_features_names['city'] = _('City') + query_features.append('country') + query_features_names['country'] = _('Country') + if not csv: student_data = instructor_analytics.basic.enrolled_students_features(course_key, query_features) response_payload = { From 646d0b9b688d93624492f6d130c13a74900b0248 Mon Sep 17 00:00:00 2001 From: AlasdairSwan Date: Mon, 16 May 2016 10:49:19 -0400 Subject: [PATCH 5/8] ECOM-4450 Added help tab and model styling to Pattern Library pages --- lms/static/sass/_build-lms-v2.scss | 2 + lms/static/sass/shared-v2/_help-tab.scss | 77 +++++++ lms/static/sass/shared-v2/_modal.scss | 275 +++++++++++++++++++++++ 3 files changed, 354 insertions(+) create mode 100644 lms/static/sass/shared-v2/_help-tab.scss create mode 100644 lms/static/sass/shared-v2/_modal.scss diff --git a/lms/static/sass/_build-lms-v2.scss b/lms/static/sass/_build-lms-v2.scss index 470ce959c7..0ac909b878 100644 --- a/lms/static/sass/_build-lms-v2.scss +++ b/lms/static/sass/_build-lms-v2.scss @@ -13,3 +13,5 @@ @import 'shared-v2/navigation'; @import 'shared-v2/header'; @import 'shared-v2/footer'; +@import 'shared-v2/modal'; +@import 'shared-v2/help-tab'; diff --git a/lms/static/sass/shared-v2/_help-tab.scss b/lms/static/sass/shared-v2/_help-tab.scss new file mode 100644 index 0000000000..90e4258415 --- /dev/null +++ b/lms/static/sass/shared-v2/_help-tab.scss @@ -0,0 +1,77 @@ +.help-tab { + @include transform(rotate(-90deg)); + @include transform-origin(0 0); + z-index: z-index(front); + top: 250px; + left: 0; + position: fixed; + + a:link, + a:visited { + cursor: pointer; + border: 1px solid palette(grayscale, light); + border-top-style: none; + border-radius: 0 0 ($baseline/2) + px ($baseline/2) + px; + background: transparentize(palette(grayscale, white-t), 0.25); + color: transparentize(palette(grayscale-cool, x-dark), 0.25); + font-weight: bold; + text-decoration: none; + padding: 6px 22px 11px; + display: inline-block; + + &:hover, + &:focus { + color: palette(grayscale, white-t); + background: palette(primary, base); + } + } +} + +.help-buttons { + padding: ($baseline/2) + px ($baseline*2.5) + px; + + a:link, a:visited { + padding: ($baseline*0.75) + px 0; + text-align: center; + cursor: pointer; + background: palette(grayscale, white-t); + text-decoration: none; + display: block; + border: 1px solid palette(grayscale, light); + + &#feedback_link_problem { + border-bottom-style: none; + border-radius: ($baseline/2) + px ($baseline/2) + px 0 0; + } + + &#feedback_link_question { + border-top-style: none; + border-radius: 0 0 ($baseline/2) + px ($baseline/2) + px; + } + + &:hover, &:focus { + color: palette(grayscale, white-t); + background: palette(primary, base); + } + } +} + +#feedback_form { + input, + textarea { + font: { + size: font-size(base); + family: $font-family-sans-serif; + } + line-height: 1.4; + } + textarea[name="details"] { + height: 150px; + } +} + +#feedback_success_wrapper { + p { + padding: 0 $baseline + px $baseline + px $baseline + px; + } +} diff --git a/lms/static/sass/shared-v2/_modal.scss b/lms/static/sass/shared-v2/_modal.scss new file mode 100644 index 0000000000..7aa623f8fa --- /dev/null +++ b/lms/static/sass/shared-v2/_modal.scss @@ -0,0 +1,275 @@ +#lean_overlay { + @include background-image(radial-gradient(circle at 50% 30%, $shadow-d1, $shadow-d2)); + background: transparent; + display: none; + height:100%; + left: 0; + position: fixed; + top: 0; + width:100%; + z-index: 100; +} + +.modal { + @include span(5); + z-index: z-index(mid-front); + display: none; + position: absolute; + left: 50%; + padding: 8px; + border-radius: 3px; + box-shadow: 0 0px 5px 0 $shadow-d1; + background: $gray-d2; + color: $base-font-color; + + .inner-wrapper { + z-index: z-index(mid-front); + background: $modal-bg-color; + border-radius: 0; + border: 1px solid rgba(0, 0, 0, 0.9); + box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.7); + overflow: hidden; + padding-left: ($baseline/2) + px; + padding-right: ($baseline/2) + px; + padding-bottom: ($baseline/2) + px; + position: relative; + + p { + font-size: font-size(small); + line-height: 1.4; + } + + a { + &:hover, + &:focus { + text-decoration: underline; + } + } + + .sr { + @extend .sr-only; + } + + header { + z-index: z-index(mid-front); + margin-bottom: ($baseline*1.5) + px; + overflow: hidden; + padding: 28px $baseline + px 0; + position: relative; + + &::before { + @include background-image(radial-gradient(50% 50%, circle closest-side, rgba(255,255,255, 0.8) 0%, rgba(255,255,255, 0) 100%)); + content: ""; + display: block; + height: 400px; + left: 0; + margin: 0 auto; + position: absolute; + top: -140px; + width: 100%; + z-index: 1; + } + + hr { + @include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%, + rgba(255,255,255, 0.8) 50%, + rgba(255,255,255, 0))); + height: 1px; + width: 100%; + border: none; + margin: 0; + position: relative; + z-index: 2; + + &::after { + @include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%, + rgba(200,200,200, 1) 50%, + rgba(200,200,200, 0))); + height: 1px; + width: 100%; + bottom: 0; + content: ''; + display: block; + position: absolute; + top: -1px; + } + } + + h2 { + @extend .hd-4; + position: relative; + text-align: center; + text-shadow: 0 1px rgba(255,255,255, 0.4); + z-index: 2; + text-transform: uppercase; + font-family: $font-family-serif; + + .edx { + text-transform: none; + } + } + } + + .modal-form-error { + background: tint($red,90%); + border: 1px solid rgb(143, 14, 14); + color: rgb(143, 14, 14); + display: none; + margin-bottom: $baseline + px; + padding: 12px; + } + + .notice { + background: $yellow; + border: 1px solid darken($yellow, 60%); + color: darken($yellow, 60%); + display: none; + margin-bottom: $baseline + px; + padding: 12px; + } + + .activation-message, .message { + padding: 0 ($baseline*2) + px ($baseline/2) + px; + + p { + margin-bottom: ($baseline/2) + px; + } + } + + form { + margin-bottom: 12px; + padding: 0 ($baseline*2) + px $baseline + px; + position: relative; + z-index: 2; + + .input-group { + @include clearfix(); + border-bottom: 1px solid rgb(210,210,210); + box-shadow: 0 1px 0 0 rgba(255,255,255, 0.6); + margin-bottom: ($baseline*1.5) + px; + padding-bottom: ($baseline/2) + px; + } + + label { + color: $text-color; + font: { + family: $font-family-serif; + style: italic; + } + line-height: 1.6; + + &.field-error { + display: block; + color: #8F0E0E; + + + input, + textarea { + border: 1px solid #CA1111; + color: #8F0E0E; + } + } + } + + input[type="checkbox"] { + margin-right: ($baseline/4) + px; + } + + textarea { + background: rgb(255,255,255); + display: block; + height: 45px; + margin-bottom: $baseline + px; + width: 100%; + } + + input[type="email"], + input[type="text"], + input[type="password"] { + background: rgb(255,255,255); + display: block; + height: 45px; + margin-bottom: $baseline + px; + width: 100%; + } + + .submit { + padding-top: ($baseline/2) + px; + + input[type="submit"] { + @extend .btn-brand; + display: block; + height: auto; + margin: 0 auto; + width: 100%; + white-space: normal; + } + } + } + + .close-modal { + @include transition(all 0.15s ease-out 0s); + border-radius: 2px; + cursor: pointer; + display: inline-block; + padding: 10px; + position: absolute; + right: 2px; + top: 0px; + z-index: z-index(front); + color: $lighter-base-font-color; + font: { + size: font-size(large); + family: $font-family-sans-serif; + } + line-height: 1; + text-align: center; + border: none; + background: transparent; + text-shadow: none; + letter-spacing: 0; + text-transform: none; + + &:hover, + &:focus { + color: $base-font-color; + text-decoration: none; + opacity: 0.8; + } + + &:focus { + border: none !important; + } + } + } + + #help_wrapper, + #feedback_form_wrapper, + .discussion-alert-wrapper { + padding: 0 ($baseline*1.5) + px ($baseline*1.5) + px ($baseline*1.5) + px; + + header { + margin-bottom: $baseline + px; + padding-right: 0; + padding-left: 0; + } + + .note { + font: { + size: font-size(x-small); + family: $font-family-sans-serif; + } + line-height: 1.475; + margin-top: ($baseline/2) + px; + color: $lighter-base-font-color; + } + } + + .tip { + font-size: font-size(x-small); + display: block; + color: $dark-gray; + } +} + +.leanModal_box { + @extend .modal; +} From c6249f4227c0c154e1e2bb6a5f6a0d9c4dc6a7b5 Mon Sep 17 00:00:00 2001 From: Matt Drayer Date: Mon, 16 May 2016 10:30:17 -0400 Subject: [PATCH 6/8] mattdrayer/WL-466: Use LMS Courses API --- lms/static/js/commerce/views/receipt_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/js/commerce/views/receipt_view.js b/lms/static/js/commerce/views/receipt_view.js index 9f8bc6b2ef..961579c2eb 100644 --- a/lms/static/js/commerce/views/receipt_view.js +++ b/lms/static/js/commerce/views/receipt_view.js @@ -158,7 +158,7 @@ var edx = edx || {}; * @return {object} JQuery Promise. */ getCourseData: function (courseId) { - var courseDetailUrl = '/api/course_structure/v0/courses/{courseId}/'; + var courseDetailUrl = '/api/courses/v1/courses/{courseId}/'; return $.ajax({ url: edx.StringUtils.interpolate(courseDetailUrl, {courseId: courseId}), type: 'GET', From 7a0d7818bc90c9ad7d82e2900f159e9d4f2ec85d Mon Sep 17 00:00:00 2001 From: AlasdairSwan Date: Mon, 16 May 2016 19:44:26 -0400 Subject: [PATCH 7/8] Offscreen a11y test fix --- lms/static/sass/elements/_program-card.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/sass/elements/_program-card.scss b/lms/static/sass/elements/_program-card.scss index d8f355dfe9..fd2353eca5 100644 --- a/lms/static/sass/elements/_program-card.scss +++ b/lms/static/sass/elements/_program-card.scss @@ -2,7 +2,7 @@ @include left(0); display: inline-block; position: absolute; - top: -1000px; + top: -999999px; overflow: hidden; } From f2ffbab0ec2ce894d645f303aec096340d8d2cc4 Mon Sep 17 00:00:00 2001 From: Matt Drayer Date: Tue, 17 May 2016 11:46:48 -0400 Subject: [PATCH 8/8] mattdrayer: Add receipt page + JS test fixes --- lms/static/js/spec/commerce/receipt_view_spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lms/static/js/spec/commerce/receipt_view_spec.js b/lms/static/js/spec/commerce/receipt_view_spec.js index f4abef3c49..2569ffc56d 100644 --- a/lms/static/js/spec/commerce/receipt_view_spec.js +++ b/lms/static/js/spec/commerce/receipt_view_spec.js @@ -36,7 +36,7 @@ define([ mockRequests(requests, 'GET', orderUrlFormat, data); mockRequests( - requests, 'GET', '/api/course_structure/v0/courses/course-v1:edx+dummy+2015_T3/', courseResponseData + requests, 'GET', '/api/courses/v1/courses/course-v1:edx+dummy+2015_T3/', courseResponseData ); mockRequests(requests, 'GET', '/api/credit/v1/providers/edx/', providerResponseData); @@ -81,6 +81,7 @@ define([ }, { "name": "course_key", + "code": "course_key", "value": "course-v1:edx+dummy+2015_T3" }, { @@ -135,7 +136,7 @@ define([ "org": "edx", "run": "2015_T2", "course": "CS420", - "uri": "http://test.com/api/course_structure/v0/courses/course-v1:edx+dummy+2015_T3/", + "uri": "http://test.com/api/courses/v1/courses/course-v1:edx+dummy+2015_T3/", "image_url": "/test.jpg", "start": "2030-01-01T00:00:00Z", "end": null