diff --git a/.pylintrc b/.pylintrc index 2f2be69eb0..49fcf80eb9 100644 --- a/.pylintrc +++ b/.pylintrc @@ -35,6 +35,7 @@ load-plugins= # it should appear only once). disable= # C0301: Line too long +# C0302: Too many lines in module # W0141: Used builtin function 'map' # W0142: Used * or ** magic # R0201: Method could be a function @@ -42,8 +43,11 @@ disable= # R0902: Too many instance attributes # R0903: Too few public methods (1/2) # R0904: Too many public methods +# R0911: Too many return statements +# R0912: Too many branches # R0913: Too many arguments - C0301,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913 +# R0914: Too many local variables + C0301,C0302,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914 [REPORTS] @@ -92,7 +96,7 @@ zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. -generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size +generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size,content [BASIC] diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index db7294c14c..558294e890 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -21,8 +21,7 @@ Feature: Advanced (manual) course policy Scenario: Test editing key value Given I am on the Advanced Course Settings page in Studio - When I edit the value of a policy key - And I press the "Save" notification button + When I edit the value of a policy key and save Then the policy key value is changed And I reload the page Then the policy key value is changed diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 16562b6b15..6fb102faea 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -51,6 +51,11 @@ def edit_the_value_of_a_policy_key(step): e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X') +@step(u'I edit the value of a policy key and save$') +def edit_the_value_of_a_policy_key(step): + change_display_name_value(step, '"foo"') + + @step('I create a JSON object as a value$') def create_JSON_object(step): change_display_name_value(step, '{"key": "value", "key_2": "value_2"}') @@ -96,7 +101,7 @@ def the_policy_key_value_is_unchanged(step): @step(u'the policy key value is changed$') def the_policy_key_value_is_changed(step): - assert_equal(get_display_name_value(), '"Robot Super Course X"') + assert_equal(get_display_name_value(), '"foo"') ############# HELPERS ############### diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py index 9eb5b0951d..d69266b7de 100644 --- a/cms/djangoapps/contentstore/features/course-settings.py +++ b/cms/djangoapps/contentstore/features/course-settings.py @@ -18,8 +18,8 @@ COURSE_END_TIME_CSS = "#course-end-time" ENROLLMENT_START_TIME_CSS = "#course-enrollment-start-time" ENROLLMENT_END_TIME_CSS = "#course-enrollment-end-time" -DUMMY_TIME = "3:30pm" -DEFAULT_TIME = "12:00am" +DUMMY_TIME = "15:30" +DEFAULT_TIME = "00:00" ############### ACTIONS #################### diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index fca14e21f0..59c5a37b33 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -38,7 +38,7 @@ def i_click_the_edit_link_for_the_release_date(step): @step('I save a new section release date$') def i_save_a_new_section_release_date(step): set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013', - 'input.start-time.time.ui-timepicker-input', '12:00am') + 'input.start-time.time.ui-timepicker-input', '00:00') world.browser.click_link_by_text('Save') @@ -105,7 +105,7 @@ def the_section_release_date_picker_not_visible(step): def the_section_release_date_is_updated(step): css = 'span.published-status' status_text = world.css_text(css) - assert_equal(status_text, 'Will Release: 12/25/2013 at 12:00am') + assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC') ############ HELPER METHODS ################### diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 1ec43e6971..f9e5b52bb2 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -57,18 +57,18 @@ def i_see_complete_subsection_name_with_quote_in_editor(step): @step('I have set a release date and due date in different years$') def test_have_set_dates_in_different_years(step): - set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '3:00am') + set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '03:00') world.css_click('.set-date') # Use a year in the past so that current year will always be different. - set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '4:00am') + set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '04:00') @step('I see the correct dates$') def i_see_the_correct_dates(step): assert_equal('12/25/2011', world.css_find('input#start_date').first.value) - assert_equal('3:00am', world.css_find('input#start_time').first.value) + assert_equal('03:00', world.css_find('input#start_time').first.value) assert_equal('01/02/2012', world.css_find('input#due_date').first.value) - assert_equal('4:00am', world.css_find('input#due_time').first.value) + assert_equal('04:00', world.css_find('input#due_time').first.value) @step('I mark it as Homework$') diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b9406c288c..60aed13c8d 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -93,6 +93,69 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): return cnt + def test_draft_metadata(self): + ''' + This verifies a bug we had where inherited metadata was getting written to the + module as 'own-metadata' when publishing. Also verifies the metadata inheritance is + properly computed + ''' + store = modulestore() + draft_store = modulestore('draft') + import_from_xml(store, 'common/test/data/', ['simple']) + + course = draft_store.get_item(Location(['i4x', 'edX', 'simple', + 'course', '2012_Fall', None]), depth=None) + html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) + + self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod) + self.assertNotIn('graceperiod', own_metadata(html_module)) + + draft_store.clone_item(html_module.location, html_module.location) + + # refetch to check metadata + html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) + + self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod) + self.assertNotIn('graceperiod', own_metadata(html_module)) + + # publish module + draft_store.publish(html_module.location, 0) + + # refetch to check metadata + html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) + + self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod) + self.assertNotIn('graceperiod', own_metadata(html_module)) + + # put back in draft and change metadata and see if it's now marked as 'own_metadata' + draft_store.clone_item(html_module.location, html_module.location) + html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) + + new_graceperiod = timedelta(**{'hours': 1}) + + self.assertNotIn('graceperiod', own_metadata(html_module)) + html_module.lms.graceperiod = new_graceperiod + self.assertIn('graceperiod', own_metadata(html_module)) + self.assertEqual(html_module.lms.graceperiod, new_graceperiod) + + draft_store.update_metadata(html_module.location, own_metadata(html_module)) + + # read back to make sure it reads as 'own-metadata' + html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) + + self.assertIn('graceperiod', own_metadata(html_module)) + self.assertEqual(html_module.lms.graceperiod, new_graceperiod) + + # republish + draft_store.publish(html_module.location, 0) + + # and re-read and verify 'own-metadata' + draft_store.clone_item(html_module.location, html_module.location) + html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) + + self.assertIn('graceperiod', own_metadata(html_module)) + self.assertEqual(html_module.lms.graceperiod, new_graceperiod) + def test_get_depth_with_drafts(self): import_from_xml(modulestore(), 'common/test/data/', ['simple']) @@ -566,6 +629,113 @@ class ContentStoreTest(ModuleStoreTestCase): self.assertIn('markdown', context, "markdown is missing from context") self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields") + def test_cms_imported_course_walkthrough(self): + """ + Import and walk through some common URL endpoints. This just verifies non-500 and no other + correct behavior, so it is not a deep test + """ + import_from_xml(modulestore(), 'common/test/data/', ['simple']) + loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]) + resp = self.client.get(reverse('course_index', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + + self.assertEqual(200, resp.status_code) + self.assertContains(resp, 'Chapter 2') + + # go to various pages + + # import page + resp = self.client.get(reverse('import_course', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # export page + resp = self.client.get(reverse('export_course', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # manage users + resp = self.client.get(reverse('manage_users', + kwargs={'location': loc.url()})) + self.assertEqual(200, resp.status_code) + + # course info + resp = self.client.get(reverse('course_info', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # settings_details + resp = self.client.get(reverse('settings_details', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # settings_details + resp = self.client.get(reverse('settings_grading', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # static_pages + resp = self.client.get(reverse('static_pages', + kwargs={'org': loc.org, + 'course': loc.course, + 'coursename': loc.name})) + self.assertEqual(200, resp.status_code) + + # static_pages + resp = self.client.get(reverse('asset_index', + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) + self.assertEqual(200, resp.status_code) + + # go look at a subsection page + subsection_location = loc._replace(category='sequential', name='test_sequence') + resp = self.client.get(reverse('edit_subsection', + kwargs={'location': subsection_location.url()})) + self.assertEqual(200, resp.status_code) + + # go look at the Edit page + unit_location = loc._replace(category='vertical', name='test_vertical') + resp = self.client.get(reverse('edit_unit', + kwargs={'location': unit_location.url()})) + self.assertEqual(200, resp.status_code) + + # delete a component + del_loc = loc._replace(category='html', name='test_html') + resp = self.client.post(reverse('delete_item'), + json.dumps({'id': del_loc.url()}), "application/json") + self.assertEqual(200, resp.status_code) + + # delete a unit + del_loc = loc._replace(category='vertical', name='test_vertical') + resp = self.client.post(reverse('delete_item'), + json.dumps({'id': del_loc.url()}), "application/json") + self.assertEqual(200, resp.status_code) + + # delete a unit + del_loc = loc._replace(category='sequential', name='test_sequence') + resp = self.client.post(reverse('delete_item'), + json.dumps({'id': del_loc.url()}), "application/json") + self.assertEqual(200, resp.status_code) + + # delete a chapter + del_loc = loc._replace(category='chapter', name='chapter_2') + resp = self.client.post(reverse('delete_item'), + json.dumps({'id': del_loc.url()}), "application/json") + self.assertEqual(200, resp.status_code) + def test_import_metadata_with_attempts_empty_string(self): import_from_xml(modulestore(), 'common/test/data/', ['simple']) module_store = modulestore('direct') diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 9a5e80bd40..8e1d2e5bfa 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -121,6 +121,12 @@ def howitworks(request): else: return render_to_response('howitworks.html', {}) + +# static/proof-of-concept views +def ux_alerts(request): + return render_to_response('ux-alerts.html', {}) + + # ==== Views for any logged-in user ================================== diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 3337fff332..4140beb2da 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -1,3 +1,5 @@ +if (!window.CmsUtils) window.CmsUtils = {}; + var $body; var $modal; var $modalCover; @@ -48,6 +50,10 @@ $(document).ready(function () { (e).preventDefault(); }); + // alerts/notifications - manual close + $('.action-alert-close, .alert.has-actions .nav-actions a').bind('click', hideAlert); + $('.action-notification-close').bind('click', hideNotification); + // nav - dropdown related $body.click(function (e) { $('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown'); @@ -87,7 +93,7 @@ $(document).ready(function () { $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); // tender feedback window scrolling - $('a.show-tender').bind('click', smoothScrollTop); + $('a.show-tender').bind('click', window.CmsUtils.smoothScrollTop); // toggling footer additional support $('.cta-show-sock').bind('click', toggleSock); @@ -159,21 +165,24 @@ $(document).ready(function () { function smoothScrollLink(e) { (e).preventDefault(); - $.smoothScroll({ - offset: -200, - easing: 'swing', + $.smoothScroll({ + offset: -200, + easing: 'swing', speed: 1000, scrollElement: null, scrollTarget: $(this).attr('href') }); } -function smoothScrollTop(e) { +// On AWS instances, this base.js gets wrapped in a separate scope as part of Django static +// pipelining (note, this doesn't happen on local runtimes). So if we set it on window, +// when we can access it from other scopes (namely Course Advanced Settings). +window.CmsUtils.smoothScrollTop = function (e) { (e).preventDefault(); - $.smoothScroll({ - offset: -200, - easing: 'swing', + $.smoothScroll({ + offset: -200, + easing: 'swing', speed: 1000, scrollElement: null, scrollTarget: $('#view-top') @@ -483,9 +492,9 @@ function toggleSock(e) { $sock.toggleClass('is-shown'); $sockContent.toggle('fast'); - $.smoothScroll({ - offset: -200, - easing: 'swing', + $.smoothScroll({ + offset: -200, + easing: 'swing', speed: 1000, scrollElement: null, scrollTarget: $sock @@ -538,6 +547,17 @@ function removeDateSetter(e) { $block.find('.time').val(''); } + +function hideNotification(e) { + (e).preventDefault(); + $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true'); +} + +function hideAlert(e) { + (e).preventDefault(); + $(this).closest('.wrapper-alert').removeClass('is-shown'); +} + function showToastMessage(message, $button, lifespan) { var $toast = $('
'); var $closeBtn = $('×'); @@ -826,7 +846,7 @@ function saveSetSectionScheduleDate(e) { data: JSON.stringify({ 'id': id, 'metadata': {'start': start}}) }).success(function () { var $thisSection = $('.courseware-section[data-id="' + id + '"]'); - $thisSection.find('.section-published-date').html('Will Release: ' + input_date + ' at ' + input_time + 'Edit'); + $thisSection.find('.section-published-date').html('Will Release: ' + input_date + ' at ' + input_time + ' UTCEdit'); $thisSection.find('.section-published-date').animate({ 'background-color': 'rgb(182,37,104)' }, 300).animate({ @@ -839,4 +859,4 @@ function saveSetSectionScheduleDate(e) { hideModal(); }); -} \ No newline at end of file +} diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 52c5ed78d0..c1392831b8 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -32,7 +32,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ var listEle$ = this.$el.find('.course-advanced-policy-list'); listEle$.empty(); - + // b/c we've deleted all old fields, clear the map and repopulate this.fieldToSelectorMap = {}; this.selectorToField = {}; @@ -101,13 +101,13 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ }); }, showMessage: function (type) { - this.$el.find(".message-status").removeClass("is-shown"); + $(".wrapper-alert").removeClass("is-shown"); if (type) { if (type === this.error_saving) { - this.$el.find(".message-status.error").addClass("is-shown"); + $(".wrapper-alert-error").addClass("is-shown").attr('aria-hidden','false'); } else if (type === this.successful_changes) { - this.$el.find(".message-status.confirm").addClass("is-shown"); + $(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false'); this.hideSaveCancelButtons(); } } @@ -117,17 +117,20 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ } }, showSaveCancelButtons: function(event) { - if (!this.buttonsVisible) { + if (!this.notificationBarShowing) { this.$el.find(".message-status").removeClass("is-shown"); - $('.wrapper-notification').addClass('is-shown'); - this.buttonsVisible = true; + $('.wrapper-notification').removeClass('is-hiding').addClass('is-shown').attr('aria-hidden','false'); + this.notificationBarShowing = true; } }, hideSaveCancelButtons: function() { - $('.wrapper-notification').removeClass('is-shown'); - this.buttonsVisible = false; + if (this.notificationBarShowing) { + $('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden','true'); + this.notificationBarShowing = false; + } }, saveView : function(event) { + window.CmsUtils.smoothScrollTop(event); // TODO one last verification scan: // call validateKey on each to ensure proper format // check for dupes @@ -146,6 +149,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ }); }, revertView : function(event) { + event.preventDefault(); var self = event.data; self.model.deleteKeys = []; self.model.clear({silent : true}); @@ -158,7 +162,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ var newKeyId = _.uniqueId('policy_key_'), newEle = this.template({ key : key, value : JSON.stringify(value, null, 4), keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')}); - + this.fieldToSelectorMap[key] = newKeyId; this.selectorToField[newKeyId] = key; return newEle; @@ -169,4 +173,4 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ blurInput : function(event) { $(event.target).prev().removeClass("is-focused"); } -}); \ No newline at end of file +}); diff --git a/cms/static/js/views/settings/main_settings_view.js b/cms/static/js/views/settings/main_settings_view.js index 3e1690f0b6..5c7e605545 100644 --- a/cms/static/js/views/settings/main_settings_view.js +++ b/cms/static/js/views/settings/main_settings_view.js @@ -110,7 +110,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ }; // instrument as date and time pickers - timefield.timepicker(); + timefield.timepicker({'timeFormat' : 'H:i'}); datefield.datepicker(); // Using the change event causes savefield to be triggered twice, but it is necessary diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index cda99b676c..73519b812d 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -25,7 +25,7 @@ a { @include transition(color 0.25s ease-in-out); &:hover { - color: #cb9c40; + color: $orange-d1; } } @@ -50,11 +50,72 @@ h1 { // ==================== +// typography - basic +.title-1, .title-2, .title-3, .title-4, .title-5, .title-6 { + font-weight: 600; + color: $gray-d3; + margin: 0; + padding: 0; +} + +.title-1 { + @include font-size(32); + margin-bottom: ($baseline*1.5); +} + +.title-2 { + @include font-size(24); + margin-bottom: $baseline; +} + +.title-3 { + @include font-size(18); + margin-bottom: ($baseline/2); +} + +.title-4 { + @include font-size(14); + margin-bottom: $baseline; + font-weight: 500 +} + +.title-5 { + @include font-size(14); + color: $gray-l1; + margin-bottom: $baseline; + font-weight: 500 +} + +.title-6 { + @include font-size(14); + color: $gray-l2; + margin-bottom: $baseline; + font-weight: 500 +} + +p, ul, ol, dl { + margin-bottom: ($baseline/2); + + &:last-child { + margin-bottom: 0; + } +} + +// ==================== + +// layout - basic +.wrapper-view { + +} + +// ==================== + // layout - basic page header .wrapper-mast { + margin: ($baseline*1.5) 0 0 0; padding: 0 $baseline; position: relative; - + .mast, .metadata { @include clearfix(); @include font-size(16); @@ -62,7 +123,7 @@ h1 { max-width: $fg-max-width; min-width: $fg-min-width; width: flex-grid(12); - margin: ($baseline*1.5) auto $baseline auto; + margin: 0 auto $baseline auto; color: $gray-d2; } @@ -284,18 +345,33 @@ h1 { margin: 0 0 ($baseline/2) 0; } - .title-4 { + header { + @include clearfix(); - } - - .title-5 { + .title-2 { + width: flex-grid(5, 12); + margin: 0 flex-gutter() 0 0; + float: left; + } + .tip { + @include font-size(13); + width: flex-grid(7, 12); + float: right; + margin-top: ($baseline/2); + text-align: right; + color: $gray-l2; + } } } // layout - supplemental content .content-supplementary { + > section { + margin: 0 0 $baseline 0; + } + .bit { @include font-size(13); margin: 0 0 $baseline 0; @@ -761,10 +837,10 @@ body.js { // ==================== -// works in progress +// works in progress & testing body.hide-wip { .wip-box { display: none; } -} \ No newline at end of file +} diff --git a/cms/static/sass/_cms_mixins.scss b/cms/static/sass/_cms_mixins.scss index a25a07cb73..c11f81f79e 100644 --- a/cms/static/sass/_cms_mixins.scss +++ b/cms/static/sass/_cms_mixins.scss @@ -15,17 +15,17 @@ // mixins - grandfathered @mixin button { display: inline-block; - padding: 4px 20px 6px; - font-size: 14px; + padding: ($baseline/5) $baseline ($baseline/4); + @include font-size(14); font-weight: 700; @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0)); @include transition(background-color .15s, box-shadow .15s); &.disabled { - border: 1px solid $lightGrey !important; + border: 1px solid $gray-l1 !important; border-radius: 3px !important; - background: $lightGrey !important; - color: $darkGrey !important; + background: $gray-l1 !important; + color: $gray-d1 !important; pointer-events: none; cursor: none; &:hover { @@ -38,32 +38,111 @@ } } -@mixin blue-button { +@mixin green-button { @include button; - border: 1px solid #437fbf; + border: 1px solid $green-d1; border-radius: 3px; @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); - background-color: $blue; - color: #fff; + background-color: $green; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); + color: $white; - &:hover, &.active { - background-color: #62aaf5; - color: #fff; + &:hover { + background-color: $green-s1; + color: $white; + } + + &.disabled { + border: 1px solid $green-l3 !important; + background: $green-l3 !important; + color: $white !important; + @include box-shadow(none); } } -@mixin green-button { - @include button; - border: 1px solid #0d7011; - border-radius: 3px; - @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); - background-color: $green; - color: #fff; +@mixin blue-button { + @include button; + border: 1px solid $blue-d1; + border-radius: 3px; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + background-color: $blue; + color: $white; - &:hover { - background-color: #129416; - color: #fff; - } + &:hover, &.active { + background-color: $blue-s2; + color: $white; + } + + &.disabled { + border: 1px solid $blue-l3 !important; + background: $blue-l3 !important; + color: $white !important; + @include box-shadow(none); + } +} + +@mixin red-button { + @include button; + border: 1px solid $red-d1; + border-radius: 3px; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + background-color: $red; + color: $white; + + &:hover, &.active { + background-color: $red-s1; + color: $white; + } + + &.disabled { + border: 1px solid $red-l3 !important; + background: $red-l3 !important; + color: $white !important; + @include box-shadow(none); + } +} + +@mixin pink-button { + @include button; + border: 1px solid $pink-d1; + border-radius: 3px; + @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); + background-color: $pink; + color: $white; + + &:hover, &.active { + background-color: $pink-s1; + color: $white; + } + + &.disabled { + border: 1px solid $pink-l3 !important; + background: $pink-l3 !important; + color: $white !important; + @include box-shadow(none); + } +} + +@mixin orange-button { + @include button; + border: 1px solid $orange-d1; + border-radius: 3px; + @include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%); + background-color: $orange; + @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); + color: $gray-d2; + + &:hover { + background-color: $orange-s2; + color: $gray-d2; + } + + &.disabled { + border: 1px solid $orange-l3 !important; + background: $orange-l2 !important; + color: $gray-l1 !important; + @include box-shadow(none); + } } @mixin white-button { @@ -82,24 +161,9 @@ } } -@mixin orange-button { - @include button; - border: 1px solid #bda046; - border-radius: 3px; - @include linear-gradient(top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0) 60%); - background-color: #edbd3c; - @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); - color: #3c3c3c; - - &:hover { - background-color: #ffcd46; - color: #3c3c3c; - } -} - @mixin grey-button { @include button; - border: 1px solid $darkGrey; + border: 1px solid $gray-d2; border-radius: 3px; @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); background-color: #d1dae3; @@ -127,39 +191,17 @@ } } -@mixin green-button { - @include button; - border: 1px solid $darkGreen; - border-radius: 3px; - @include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)); - background-color: $green; - @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); - color: #fff; - - &:hover { - background-color: $brightGreen; - color: #fff; - } - - &.disabled { - border: 1px solid $disabledGreen !important; - background: $disabledGreen !important; - color: #fff !important; - @include box-shadow(none); - } -} - @mixin dark-grey-button { @include button; - border: 1px solid #1c1e20; + border: 1px solid $gray-d2; border-radius: 3px; - background: -webkit-linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0)) $extraDarkGrey; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0)) $gray-d1; box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; - color: #fff; + color: $white; &:hover { - background-color: #595f64; - color: #fff; + background-color: $gray-d4; + color: $white; } } @@ -180,7 +222,7 @@ } textarea { - min-height: 80px; + min-height: 80px; } h5 { @@ -225,7 +267,7 @@ .section-item { position: relative; display: block; - padding: 6px 8px 8px 16px; + padding: 6px 8px 8px 16px; background: #edf1f5; font-size: 13px; @@ -296,6 +338,9 @@ } } +// ==================== + +// sunsetted mixins @mixin active { @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); background-color: rgba(255, 255, 255, .3); @@ -389,4 +434,4 @@ .depth2 { z-index: 100; } .depth3 { z-index: 1000; } .depth4 { z-index: 10000; } -.depth5 { z-index: 100000; } \ No newline at end of file +.depth5 { z-index: 100000; } diff --git a/cms/static/sass/_reset.scss b/cms/static/sass/_reset.scss index 963f04bb69..ee4e76e381 100644 --- a/cms/static/sass/_reset.scss +++ b/cms/static/sass/_reset.scss @@ -62,6 +62,12 @@ table { border-spacing: 0; } +abbr[title] { + border-bottom: none; + text-decoration: none; + cursor: help; +} + // ==================== // grandfathered styles diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index 41c9e57d54..4c35bfad32 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -113,7 +113,7 @@ $green-u1: desaturate($green,15%); $green-u2: desaturate($green,30%); $green-u3: desaturate($green,45%); -$yellow: rgb(231, 214, 143); +$yellow: rgb(237, 189, 60); $yellow-l1: tint($yellow,20%); $yellow-l2: tint($yellow,40%); $yellow-l3: tint($yellow,60%); @@ -149,8 +149,13 @@ $orange-u3: desaturate($orange,45%); $shadow: rgba(0,0,0,0.2); $shadow-l1: rgba(0,0,0,0.1); +$shadow-l2: rgba(0,0,0,0.05); $shadow-d1: rgba(0,0,0,0.4); + +// specific UI +$notification-height: ($baseline*10); + // colors - inherited $baseFontColor: $gray-d2; $offBlack: #3c3c3c; @@ -167,4 +172,4 @@ $disabledGreen: rgb(124, 206, 153); $darkGreen: rgb(52, 133, 76); $lightBluishGrey: rgb(197, 207, 223); $lightBluishGrey2: rgb(213, 220, 228); -$error-red: rgb(253, 87, 87); \ No newline at end of file +$error-red: rgb(253, 87, 87); diff --git a/cms/static/sass/assets/_keyframes.scss b/cms/static/sass/assets/_keyframes.scss index 7661f18980..a756f66b2e 100644 --- a/cms/static/sass/assets/_keyframes.scss +++ b/cms/static/sass/assets/_keyframes.scss @@ -1,7 +1,133 @@ -@mixin bounce-in { +// studio animations & keyframes +// ==================== + +// rotate clockwise +@mixin rotateClockwise { + 0% { + @include transform(rotate(0deg)); + } + + 100% { + @include transform(rotate(360deg)); + } +} + +@-moz-keyframes rotateClockwise { @include rotateClockwise(); } +@-webkit-keyframes rotateClockwise { @include rotateClockwise(); } +@-o-keyframes rotateClockwise { @include rotateClockwise(); } +@keyframes rotateClockwise { @include rotateClockwise();} + +@mixin anim-rotateClockwise($duration, $timing: ease-in-out, $count: 1, $delay: 0) { + @include animation-name(rotateClockwise); + @include animation-duration($duration); + @include animation-delay($delay); + @include animation-timing-function($timing); + @include animation-iteration-count($count); + @include animation-fill-mode(both); + +} + +// ==================== + +// notifications slide up +@mixin notificationsSlideUp { + 0% { + @include transform(translateY(0)); + } + + 90% { + @include transform(translateY(-($notification-height))); + } + + 100% { + @include transform(translateY(-($notification-height*0.99))); + } +} + +@-moz-keyframes notificationsSlideUp { @include notificationsSlideUp(); } +@-webkit-keyframes notificationsSlideUp { @include notificationsSlideUp(); } +@-o-keyframes notificationsSlideUp { @include notificationsSlideUp(); } +@keyframes notificationsSlideUp { @include notificationsSlideUp();} + +@mixin anim-notificationsSlideUp($duration, $timing: ease-in-out, $count: 1, $delay: 0) { + @include animation-name(notificationsSlideUp); + @include animation-duration($duration); + @include animation-delay($delay); + @include animation-timing-function($timing); + @include animation-iteration-count($count); + @include animation-fill-mode(both); + +} + +// ==================== + +// notifications slide down +@mixin notificationsSlideDown { + 0% { + @include transform(translateY(-($notification-height*0.99))); + } + + 10% { + @include transform(translateY(-($notification-height))); + } + + 100% { + @include transform(translateY(0)); + } +} + +@-moz-keyframes notificationsSlideDown { @include notificationsSlideDown(); } +@-webkit-keyframes notificationsSlideDown { @include notificationsSlideDown(); } +@-o-keyframes notificationsSlideDown { @include notificationsSlideDown(); } +@keyframes notificationsSlideDown { @include notificationsSlideDown();} + +@mixin anim-notificationsSlideDown($duration, $timing: ease-in-out, $count: 1, $delay: 0) { + @include animation-name(notificationsSlideDown); + @include animation-duration($duration); + @include animation-delay($delay); + @include animation-timing-function($timing); + @include animation-iteration-count($count); + @include animation-fill-mode(both); +} + +// ==================== + +// notifications slide up then down +@mixin notificationsSlideUpDown { + 0%, 100% { + @include transform(translateY(0)); + } + + 15%, 85% { + @include transform(translateY(-($notification-height))); + } + + 20%, 80% { + @include transform(translateY(-($notification-height*0.99))); + } +} + +@-moz-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); } +@-webkit-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); } +@-o-keyframes notificationsSlideUpDown { @include notificationsSlideUpDown(); } +@keyframes notificationsSlideUpDown { @include notificationsSlideUpDown();} + +@mixin anim-notificationsSlideUpDown($duration, $timing: ease-in-out, $count: 1, $delay: 0) { + @include animation-name(notificationsSlideUpDown); + @include animation-duration($duration); + @include animation-delay($delay); + @include animation-timing-function($timing); + @include animation-iteration-count($count); + @include animation-fill-mode(both); +} + +// ==================== + +// bounce in +@mixin bounceIn { 0% { opacity: 0; - @include transform(scale(.3)); + @include transform(scale(0.3)); } 50% { @@ -14,14 +140,63 @@ } } -@-moz-keyframes bounce-in { @include bounce-in(); } -@-webkit-keyframes bounce-in { @include bounce-in(); } -@-o-keyframes bounce-in { @include bounce-in(); } -@keyframes bounce-in { @include bounce-in();} +@-moz-keyframes bounceIn { @include bounceIn(); } +@-webkit-keyframes bounceIn { @include bounceIn(); } +@-o-keyframes bounceIn { @include bounceIn(); } +@keyframes bounceIn { @include bounceIn();} -@mixin bounce-in-animation($duration, $timing: ease-in-out) { - @include animation-name(bounce-in); +@mixin anim-bounceIn($duration, $timing: ease-in-out, $count: 1, $delay: 0) { + @include animation-name(bounceIn); @include animation-duration($duration); + @include animation-delay($delay); @include animation-timing-function($timing); + @include animation-iteration-count($count); @include animation-fill-mode(both); } + +// ==================== + +// bounce in +@mixin bounceOut { + 0% { + opacity: 0; + @include transform(scale(0.3)); + } + + 50% { + opacity: 1; + @include transform(scale(1.05)); + } + + 100% { + @include transform(scale(1)); + } + + 0% { + @include transform(scale(1)); + } + + 50% { + opacity: 1; + @include transform(scale(1.05)); + } + + 100% { + opacity: 0; + @include transform(scale(0.3)); + } +} + +@-moz-keyframes bounceOut { @include bounceOut(); } +@-webkit-keyframes bounceOut { @include bounceOut(); } +@-o-keyframes bounceOut { @include bounceOut(); } +@keyframes bounceOut { @include bounceOut();} + +@mixin anim-bounceOut($duration, $timing: ease-in-out, $count: 1, $delay: 0) { + @include animation-name(bounceOut); + @include animation-duration($duration); + @include animation-delay($delay); + @include animation-timing-function($timing); + @include animation-iteration-count($count); + @include animation-fill-mode(both); +} \ No newline at end of file diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 1bf9329b36..a355b3a03f 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -4,6 +4,7 @@ // bourbon libs and resets @import 'bourbon/bourbon'; @import 'bourbon/addons/button'; +@import "variables"; @import 'vendor/normalize'; @import 'reset'; diff --git a/cms/static/sass/elements/_alerts.scss b/cms/static/sass/elements/_alerts.scss index 9c15f811e0..49aa015313 100644 --- a/cms/static/sass/elements/_alerts.scss +++ b/cms/static/sass/elements/_alerts.scss @@ -1,135 +1,761 @@ -// studio - elements - alerts, notifications, prompts +// studio alerts, prompts and notifications +// ==================== + +// shared +.wrapper-notification, .wrapper-alert, .prompt { + @include box-sizing(border-box); + + .copy { + @include font-size(13); + } +} + +.wrapper-notification, .wrapper-alert, .prompt { + background: $gray-d3; + + .copy { + color: $gray-l2; + + .title { + color: $white; + } + + .nav-actions { + + .action-primary { + color: $gray-d4; + } + } + } +} + +.alert, .notification, .prompt { + + // types - confirm + &.confirm { + + .nav-actions .action-primary { + @include blue-button(); + border-color: $blue-d2; + } + + a { + color: $blue; + + &:hover { + color: $blue-s2; + } + } + } + + // types - warning + &.warning { + + .nav-actions .action-primary { + @include orange-button(); + border-color: $orange-d2; + color: $gray-d4; + } + + a { + color: $orange; + + &:hover { + color: $orange-s2; + } + } + } + + // types - error + &.error { + + .nav-actions .action-primary { + @include red-button(); + border-color: $red-d2; + } + + a { + color: $red-l1; + + &:hover { + color: $red; + } + } + } + + // types - announcement + &.announcement { + + .nav-actions .action-primary { + @include blue-button(); + border-color: $blue-d2; + } + + a { + color: $blue; + + &:hover { + color: $blue-s2; + } + } + } + + // types - confirmation + &.confirmation { + + .nav-actions .action-primary { + @include green-button(); + border-color: $green-d2; + } + + a { + color: $green; + + &:hover { + color: $green-s2; + } + } + } + + // types - step required + &.step-required { + + .nav-actions .action-primary { + border-color: $pink-d2; + @include pink-button(); + } + + a { + color: $pink; + + &:hover { + color: $pink-s1; + } + } + } +} + +// prompts +.wrapper-prompt { + @include transition(all 0.05s ease-in-out); + position: fixed; + top: 0; + background: $black-t0; + width: 100%; + height: 100%; + text-align: center; + z-index: 10000; + + &:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + margin-right: -0.25em; /* Adjusts for spacing */ + } + + .prompt { + @include border-radius(($baseline/5)); + @include box-shadow(0 0 3px $shadow-d1); + display: inline-block; + vertical-align: middle; + width: $baseline*17.5; + border: 4px solid $black; + text-align: left; + + .copy { + border-top: 4px solid $blue; + padding: $baseline; + } + + .nav-actions { + @include box-shadow(inset 0 1px 2px $shadow-d1); + border-top: 1px solid $black-t1; + padding: ($baseline*0.75) $baseline; + background: $gray-d4; + + .nav-item { + display: inline-block; + margin-right: ($baseline*0.75); + + &:last-child { + margin-right: 0; + } + } + + .action-primary { + @include font-size(13); + font-weight: 600; + } + + .action-secondary { + @include font-size(13); + } + } + } + + // types of prompts - error + .prompt.error { + + .icon-error { + color: $red-l1; + } + + .copy { + border-top-color: $red-l1; + } + } + + // types of prompts - confirmation + .prompt.confirmation { + + .icon-error { + color: $green; + } + + .copy { + border-top-color: $green; + } + } + + // types of prompts - error + .prompt.warning { + + .icon-warning { + color: $orange; + } + + .copy { + border-top-color: $orange; + } + } +} + // ==================== // notifications .wrapper-notification { - @include clearfix(); - @include box-sizing(border-box); - @include transition (bottom 2.0s ease-in-out 5s); - @include box-shadow(0 -1px 2px rgba(0,0,0,0.1)); - position: fixed; - bottom: -100px; - z-index: 1000; - width: 100%; - overflow: hidden; - opacity: 0; - border-top: 1px solid $darkGrey; - padding: 20px 40px; + @include clearfix(); + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $blue); + position: fixed; + bottom: 0; + z-index: 1000; + width: 100%; + padding: $baseline ($baseline*2); - &.is-shown { - bottom: 0; - opacity: 1.0; - } + &.wrapper-notification-warning { + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $orange); - &.wrapper-notification-warning { - border-color: shade($yellow, 25%); - background: tint($yellow, 25%); - } + .icon-warning { + color: $orange; + } + } - &.wrapper-notification-error { - border-color: shade($red, 50%); - background: tint($red, 20%); - color: $white; - } + &.wrapper-notification-error { + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $red-l1); - &.wrapper-notification-confirm { - border-color: shade($green, 30%); - background: tint($green, 40%); - color: shade($green, 30%); - } + .icon-error { + color: $red-l1; + } + } + + &.wrapper-notification-confirmation { + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $green); + + .icon-confirmation { + color: $green; + } + } + + &.wrapper-notification-saving { + @include box-shadow(0 -1px 3px $shadow, inset 0 3px 1px $pink); + } + + // shorter/status notifications + &.wrapper-notification-status { + @include border-top-radius(3px); + width: ($baseline*8); + right: ($baseline); + border: 4px solid $black; + border-bottom: none; + padding: ($baseline/2) $baseline; + + .notification { + @include box-sizing(border-box); + @include clearfix(); + width: 100%; + max-width: none; + min-width: none; + + .icon, .copy { + float: none; + display: inline-block; + vertical-align: middle; + } + + .icon { + width: $baseline; + height: ($baseline*1.25); + margin-right: ($baseline*0.75); + line-height: 3rem; + } + + .copy { + + } + } + } + + // help notifications + &.wrapper-notification-help { + @include border-top-radius(3px); + width: ($baseline*14); + right: ($baseline); + border: 4px solid $black; + border-bottom: none; + padding: $baseline; + + .notification { + @include box-sizing(border-box); + @include clearfix(); + width: 100%; + max-width: none; + min-width: none; + + .icon-help { + width: $baseline; + margin-right: ($baseline*0.75); + } + + .action-notification-close { + right: 0; + } + + .copy { + width: ($baseline*10); + } + } + } } .notification { - @include box-sizing(border-box); - margin: 0 auto; - width: flex-grid(12); - max-width: $fg-max-width; - min-width: $fg-min-width; + @include box-sizing(border-box); + @include clearfix(); + margin: 0 auto; + width: flex-grid(12); + max-width: $fg-max-width; + min-width: $fg-min-width; - .copy { - float: left; - width: flex-grid(9, 12); - margin-right: flex-gutter(); - margin-top: 5px; - font-size: 14px; + strong { + font-weight: 700; + } - .icon { - display: inline-block; - vertical-align: top; - margin-right: 5px; - font-size: 20px; - } + .icon, .copy { + float: left; + display: inline-block; + vertical-align: middle; + } - p { - width: flex-grid(8, 9); - display: inline-block; - vertical-align: top; - } - } + .icon { + @include transition (color 0.5s ease-in-out); + @include font-size(22); + width: flex-grid(1, 12); + height: ($baseline*1.25); + margin-right: flex-gutter(); + text-align: right; + color: $white; + } - .actions { - float: right; - width: flex-grid(3, 12); - margin-top: ($baseline/2); - text-align: right; + .copy { + @include font-size(13); + width: flex-grid(10, 12); + color: $gray-l2; - li { - display: inline-block; - vertical-align: middle; - margin-right: 10px; + .title { + @include font-size(14); + margin-bottom: 0; + color: $white; + } + } - &:last-child { - margin-right: 0; - } - } + // with actions + &.has-actions { - .save-button { - @include blue-button; - } + .icon { + width: flex-grid(1, 12); + } - .cancel-button { - @include white-button; - } - } + .copy { + width: flex-grid(7, 12); + margin-right: flex-gutter(); + } - strong { - font-weight: 700; - } + .nav-actions { + width: flex-grid(4, 12); + float: right; + margin-top: ($baseline/4); + text-align: right; + + .nav-item { + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + } + } + + .action-primary { + @include blue-button(); + @include font-size(13); + border-color: $blue-d2; + font-weight: 600; + } + + .action-secondary { + @include font-size(13); + } + } + + &.confirmation { + + .copy { + margin-top: ($baseline/5); + } + } + + &.saving { + + .icon-saving { + @include anim-rotateClockwise(3s, linear, infinite); + width: 22px; + } + + .copy p { + @include text-sr(); + } + } +} + +// ==================== + +// alerts +.wrapper-alert { + @include box-sizing(border-box); + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue); + position: relative; + z-index: 100; + overflow: hidden; + width: 100%; + border-top: 1px solid $black; + padding: $baseline ($baseline*2) ($baseline*1.5) ($baseline*2); + background: $gray-d3; + + // needed since page load is very slow + display: none; + + // needed since page load is very slow + &.is-shown { + display: block; + } + + &.wrapper-alert-warning { + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $orange); + + .icon-warning { + color: $orange; + } + } + + &.wrapper-alert-error { + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $red-l1); + + .icon-error { + color: $red-l1; + } + } + + &.wrapper-alert-confirmation { + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $green); + + .icon-confirmation { + color: $green; + } + } + + &.wrapper-alert-announcement { + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $blue); + + .icon-announcement { + color: $blue; + } + } + + &.wrapper-alert-step-required { + @include box-shadow(0 1px 1px $white, inset 0 2px 2px $shadow-d1, inset 0 -4px 1px $pink); + + .icon-step-required { + color: $pink; + } + } } // adopted alerts .alert { - padding: 15px 20px; - margin-bottom: 30px; - border-radius: 3px; - border: 1px solid #edbd3c; - border-radius: 3px; - background: #fbf6e1; - // background: #edbd3c; - font-size: 14px; - @include clearfix; + @include font-size(14); + @include box-sizing(border-box); + @include clearfix(); + margin: 0 auto; + width: flex-grid(12); + max-width: $fg-max-width; + min-width: $fg-min-width; + color: $white; - .alert-message { - float: left; - margin-top: 4px; - } + strong { + font-weight: 700; + } - strong { - font-weight: 700; - } + .icon, .copy { + float: left; + } - .alert-action { - float: right; + .icon { + @include transition (color 0.5s ease-in-out); + @include font-size(22); + width: flex-grid(1, 12); + margin: ($baseline/4) flex-gutter() 0 0; + text-align: right; + } - &.secondary { - @include orange-button; - } - } + .copy { + @include font-size(13); + width: flex-grid(10, 12); + color: $gray-l2; + + .title { + margin-bottom: 0; + color: $white; + } + } + + // with actions + &.has-actions { + + .icon { + width: flex-grid(1, 12); + } + + .copy { + width: flex-grid(7, 12); + margin-right: flex-gutter(); + } + + .nav-actions { + width: flex-grid(4, 12); + float: right; + margin-top: ($baseline/2); + text-align: right; + + .nav-item { + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + + .action-primary { + @include font-size(13); + font-weight: 600; + } + + .action-secondary { + @include font-size(13); + } + } + } + } + + // with cancel + .action-alert-close { + @include border-bottom-radius(($baseline/5)); + position: absolute; + top: -($baseline/10); + right: $baseline; + padding: ($baseline/4) ($baseline/2) 0 ($baseline/2); + background: $gray-d4; + text-align: center; + + .label { + @include text-sr(); + } + + .icon { + @include font-size(14); + color: $white; + width: auto; + margin: 0; + padding: 2px; + } + + &:hover { + background: $gray-d1; + } + } +} + +// ==================== + +// js enabled +.js { + + // prompt set-up + .wrapper-prompt { + visibility: hidden; + pointer-events: none; + + .prompt { + opacity: 0; + } + } + + // prompt showing/hiding + &.prompt-is-shown { + + .wrapper-view { + -webkit-filter: blur(2px) grayscale(25%); + filter: blur(2px) grayscale(25%); + } + + .wrapper-prompt.is-shown { + visibility: visible; + pointer-events: auto; + + .prompt { + @include anim-bounceIn(0.5s); + opacity: 1.0; + } + } + } + + // alert showing/hiding + .wrapper-alert { + display: none; + + &.is-shown { + display: block; + } + } + + // notification showing/hiding + .wrapper-notification { + bottom: -($notification-height); + + // varying animations + &.is-shown { + @include anim-notificationsSlideUp(1s); + } + + &.is-hiding { + @include anim-notificationsSlideDown(0.25s); + } + + &.is-fleeting { + @include anim-notificationsSlideUpDown(2s); + } + } +} + +// ==================== + +// temporary +body.uxdesign.alerts { + + .content-primary, .content-supplementary { + @include box-sizing(border-box); + float: left; + } + + .content-primary { + @extend .window; + width: flex-grid(12, 12); + margin-right: flex-gutter(); + padding: $baseline ($baseline*1.5); + + > section { + margin-bottom: ($baseline*2); + + &:last-child { + margin-bottom: 0; + } + } + + ul { + + li { + @include clearfix(); + width: flex-grid(12, 12); + margin-bottom: ($baseline/4); + border-bottom: 1px solid $gray-l4; + padding-bottom: ($baseline/4); + + &:last-child { + margin-bottom: 0; + border-bottom: none; + padding-bottom: 0; + } + + a { + float: left; + width: flex-grid(5, 12); + margin-right: flex-gutter(); + } + } + } + } +} + +// ==================== + +// artifact styles +.main-wrapper { + .alert { + padding: 15px 20px; + margin-bottom: 30px; + border-radius: 3px; + border: 1px solid #edbd3c; + border-radius: 3px; + background: #fbf6e1; + // background: #edbd3c; + font-size: 14px; + @include clearfix; + + .alert-message { + float: left; + margin: 4px 0 0 0; + color: $gray-d3; + } + + strong { + font-weight: 700; + } + + .alert-action { + float: right; + + &.secondary { + @include orange-button; + } + } + } } body.error { - background: $darkGrey; - color: #3c3c3c; + background: $gray-d4; + color: $gray-d3; .primary-header { display: none; @@ -140,7 +766,7 @@ body.error { margin: 150px auto; padding: 60px 50px 90px; border-radius: 3px; - background: #fff; + background: $white; text-align: center; } @@ -148,8 +774,8 @@ body.error { float: none; margin: 0; font-size: 60px; - font-weight: 300; - color: #3c3c3c; + font-weight: 300; + color: $gray-d3; } .description { @@ -162,4 +788,4 @@ body.error { padding: 14px 40px 18px; font-size: 18px; } -} \ No newline at end of file +} diff --git a/cms/static/sass/elements/_header.scss b/cms/static/sass/elements/_header.scss index 00e2963630..6628d10c4c 100644 --- a/cms/static/sass/elements/_header.scss +++ b/cms/static/sass/elements/_header.scss @@ -5,12 +5,12 @@ margin: 0; padding: $baseline; border-bottom: 1px solid $gray; - @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); + @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.2)); background: $white; height: 76px; position: relative; width: 100%; - z-index: 10; + z-index: 1000; a { color: $baseFontColor; diff --git a/cms/static/sass/views/_account.scss b/cms/static/sass/views/_account.scss index 2be94a81ea..9b38492921 100644 --- a/cms/static/sass/views/_account.scss +++ b/cms/static/sass/views/_account.scss @@ -4,7 +4,7 @@ body.signup, body.signin { .wrapper-content { - margin: 0; + margin: ($baseline*1.5) 0 0 0; padding: 0 $baseline; position: relative; width: 100%; @@ -18,7 +18,7 @@ body.signup, body.signin { width: flex-grid(12); margin: 0 auto; color: $gray-d2; - + header { position: relative; margin-bottom: $baseline; @@ -121,7 +121,7 @@ body.signup, body.signin { @include font-size(16); height: 100%; width: 100%; - padding: ($baseline/2); + padding: ($baseline/2); &.long { width: 100%; @@ -136,15 +136,15 @@ body.signup, body.signin { } :-moz-placeholder { - color: $gray-l3; + color: $gray-l3; } ::-moz-placeholder { - color: $gray-l3; + color: $gray-l3; } - :-ms-input-placeholder { - color: $gray-l3; + :-ms-input-placeholder { + color: $gray-l3; } &:focus { diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss index 6a141fa789..6b179f0731 100644 --- a/cms/static/sass/views/_outline.scss +++ b/cms/static/sass/views/_outline.scss @@ -604,13 +604,39 @@ body.course.outline { } .picker { + @include clearfix(); margin: 30px 0 65px; + + .field { + float: left; + margin-right: ($baseline/2); + + &:first-child { + margin-left: ($baseline*5); + } + + &:last-child { + margin-right: 0; + } + + label, input { + display: block; + text-align: left; + } + + label { + @include font-size(14); + margin-bottom: ($baseline/4); + } + } } .description { + float: left; margin-top: 30px; font-size: 14px; line-height: 20px; + width: 100%; } strong { diff --git a/cms/static/sass/views/_subsection.scss b/cms/static/sass/views/_subsection.scss index cd29759db8..68e3548ea1 100644 --- a/cms/static/sass/views/_subsection.scss +++ b/cms/static/sass/views/_subsection.scss @@ -3,11 +3,41 @@ body.course.subsection { + .main-wrapper { + margin-top: ($baseline*2); + } + .unit-settings { .window-contents { padding: 10px 20px; } + .datepair { + + .field { + display: inline-block; + margin-right: ($baseline/4); + width: 45%; + + &:last-child { + margin-right: 0; + } + + label, input { + display: block; + text-align: left; + } + + input { + width: 100%; + } + + label { + margin-bottom: ($baseline/4); + } + } + } + .unit-actions { border-bottom: none; padding-bottom: 0; @@ -74,7 +104,7 @@ body.course.subsection { } .window-contents { - display: none; + display: none; } } @@ -232,6 +262,7 @@ body.course.subsection { .remove-date { display: block; + margin-top: ($baseline/4); } } @@ -259,7 +290,7 @@ body.course.subsection { background-position: 0 -50px; .hidden { - background-position: 0 -5px; + background-position: 0 -5px; } } } @@ -369,4 +400,4 @@ body.course.subsection { } } } -} \ No newline at end of file +} diff --git a/cms/static/sass/views/_unit.scss b/cms/static/sass/views/_unit.scss index e74690d9ec..06849f851c 100644 --- a/cms/static/sass/views/_unit.scss +++ b/cms/static/sass/views/_unit.scss @@ -3,9 +3,8 @@ body.course.unit { - .unit .main-wrapper { - @include clearfix(); - margin: 40px; + .main-wrapper { + margin-top: ($baseline*2); } //Problem Selector tab menu requirements @@ -31,7 +30,7 @@ body.course.unit { } .unit-body { - + .unit-name-input { padding: 20px 40px; @@ -44,7 +43,7 @@ body.course.unit { font-size: 20px; } } - + .breadcrumbs { border-radius: 3px 3px 0 0; border-bottom: 1px solid #cbd1db; @@ -189,10 +188,10 @@ body.course.unit { @include clearfix; a { - position: relative; + position: relative; border: 1px solid $darkGreen; background: tint($green,20%); - color: #fff; + color: #fff; &:hover { background: $brightGreen; @@ -254,8 +253,8 @@ body.course.unit { @include transition (none); &:hover { - background: tint($green,30%); - color: #fff; + background: tint($green,30%); + color: #fff; @include transition(background-color .15s); } } @@ -263,7 +262,7 @@ body.course.unit { li { border:none; border-bottom: 1px dashed $lightGrey; - color: #fff; + color: #fff; } li:first-child { @@ -326,7 +325,7 @@ body.course.unit { } } - // specific editor types + // specific editor types .empty { a { @@ -337,20 +336,20 @@ body.course.unit { &:hover { - background: tint($green,30%); + background: tint($green,30%); color: #fff; } } } } - .new-component { + .new-component { text-align: center; h5 { color: $darkGreen; } - + } } } @@ -374,7 +373,7 @@ body.course.unit { &.editing { border: 1px solid $lightBluishGrey2; z-index: auto; - + .drag-handle, .component-actions { display: none; @@ -434,7 +433,7 @@ body.course.unit { label { display: inline-block; - margin-right: 10px; + margin-right: 10px; } } @@ -528,7 +527,7 @@ body.course.unit { } .window-contents { - display: none; + display: none; } } @@ -678,4 +677,4 @@ body.unit { padding-top: 0; } } -} \ No newline at end of file +} diff --git a/cms/templates/base.html b/cms/templates/base.html index 0809795f70..4517790622 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -49,20 +49,30 @@ - <%include file="widgets/header.html" /> - - <%block name="content"> - - % if user.is_authenticated(): - <%include file="widgets/sock.html" /> - % endif - - <%include file="widgets/footer.html" /> - <%include file="widgets/tender.html" /> + +
+ <%include file="widgets/header.html" /> + + <%block name="view_alerts"> + <%block name="view_banners"> + + <%block name="content"> + + % if user.is_authenticated(): + <%include file="widgets/sock.html" /> + % endif + + <%include file="widgets/footer.html" /> + <%include file="widgets/tender.html" /> + + <%block name="view_notifications"> +
+ + <%block name="view_prompts"> <%block name="jsextra"> diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index 80385de829..901e0a8008 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -33,17 +33,22 @@

Subsection Settings

-
- - +
+ + +
+
+ + +
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start: % if parent_item.lms.start is None:

The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset. % else:

The date above differs from the release date of ${parent_item.display_name_with_default} – - ${get_time_struct_display(parent_item.lms.start, '%m/%d/%Y at %I:%M %p')}. + ${get_time_struct_display(parent_item.lms.start, '%m/%d/%Y at %H:%M UTC')}. % endif Sync to ${parent_item.display_name_with_default}.

% endif @@ -56,14 +61,17 @@
- Set a due date
-

+

+ +
+
+ - Remove due date -

+
+ Remove due date
diff --git a/cms/templates/index.html b/cms/templates/index.html index 8b14ea179a..916720f4e7 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -69,7 +69,7 @@
% if user.is_active:
- +

Section Release Date

- - +
+ + +
+
+ + +
+

On the date set above, this section – – will be released to students. Any units marked private will only be visible to admins.

diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index acbbb7cd35..c40427c9ec 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -42,13 +42,17 @@ editor.render(); <%block name="content"> -
-
-
+
+
+
Settings

Advanced Settings

-
+
+
+
+
+
@@ -69,7 +73,7 @@ editor.render();

Warning: Do not modify these policies unless you are familiar with their purpose.

    - +
@@ -100,23 +104,61 @@ editor.render();
+ +<%block name="view_notifications"> -
-
-
- + \ No newline at end of file +
+
diff --git a/cms/urls.py b/cms/urls.py index e1eae3352a..06569e4178 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -116,6 +116,8 @@ urlpatterns += ( url(r'^logout$', 'student.views.logout_user', name='logout'), + # static/proof-of-concept views + url(r'^ux-alerts$', 'contentstore.views.ux_alerts', name='ux-alerts') ) if settings.ENABLE_JASMINE: diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index d398dfef0d..3f6db354d6 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -108,6 +108,22 @@ def add_histogram(get_html, module, user): histogram = grade_histogram(module_id) render_histogram = len(histogram) > 0 + if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'): + [filepath, filename] = getattr(module.descriptor, 'xml_attributes', {}).get('filename', ['', None]) + osfs = module.system.filestore + if filename is not None and osfs.exists(filename): + # if original, unmangled filename exists then use it (github + # doesn't like symlinks) + filepath = filename + data_dir = osfs.root_path.rsplit('/')[-1] + giturl = getattr(module.lms, 'giturl', '') or 'https://github.com/MITx' + edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath) + else: + edit_link = False + # Need to define all the variables that are about to be used + giturl = "" + data_dir = "" + source_file = module.lms.source_file # source used to generate the problem XML, eg latex or word # useful to indicate to staff if problem has been released or not @@ -121,12 +137,15 @@ def add_histogram(get_html, module, user): staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields], 'lms_fields': [(field.name, getattr(module.lms, field.name)) for field in module.lms.fields], + 'xml_attributes' : getattr(module.descriptor, 'xml_attributes', {}), 'location': module.location, 'xqa_key': module.lms.xqa_key, 'source_file': source_file, + 'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file), 'category': str(module.__class__.__name__), # Template uses element_id in js function names, so can't allow dashes 'element_id': module.location.html_id().replace('-', '_'), + 'edit_link': edit_link, 'user': user, 'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'), 'histogram': json.dumps(histogram), diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 4712bbe426..7f73be601a 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -180,6 +180,7 @@ class CourseFields(object): has_children = True checklists = List(scope=Scope.settings) info_sidebar_name = String(scope=Scope.settings, default='Course Handouts') + show_timezone = Boolean(help="True if timezones should be shown on dates in the courseware", scope=Scope.settings, default=True) # An extra property is used rather than the wiki_slug/number because # there are courses that change the number for different runs. This allows diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index cfce5eb7db..3682dea55a 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -2,6 +2,7 @@ from datetime import datetime from . import ModuleStoreBase, Location, namedtuple_to_son from .exceptions import ItemNotFoundError +from .inheritance import own_metadata import logging DRAFT = 'draft' @@ -181,7 +182,7 @@ class DraftModuleStore(ModuleStoreBase): draft.cms.published_by = published_by_id super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data) super(DraftModuleStore, self).update_children(location, draft._model_data._kvs._children) - super(DraftModuleStore, self).update_metadata(location, draft._model_data._kvs._metadata) + super(DraftModuleStore, self).update_metadata(location, own_metadata(draft)) self.delete_item(location) def unpublish(self, location): diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index d819abe367..927f01d543 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -8,7 +8,8 @@ INHERITABLE_METADATA = ( # How many days early to show a course element to beta testers (float) # intended to be set per-course, but can be overridden in for specific # elements. Can be a float. - 'days_early_for_beta' + 'days_early_for_beta', + 'giturl' # for git edit link ) def compute_inherited_metadata(descriptor): diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index d584438efe..653a7ca22a 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -176,7 +176,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem): model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location)) module = class_(self, location, model_data) if self.cached_metadata is not None: - metadata_to_inherit = self.cached_metadata.get(location.url(), {}) + # parent container pointers don't differentiate between draft and non-draft + # so when we do the lookup, we should do so with a non-draft location + non_draft_loc = location._replace(revision=None) + metadata_to_inherit = self.cached_metadata.get(non_draft_loc.url(), {}) inherit_metadata(module, metadata_to_inherit) return module except: @@ -260,11 +263,11 @@ class MongoModuleStore(ModuleStoreBase): # get all collections in the course, this query should not return any leaf nodes # note this is a bit ugly as when we add new categories of containers, we have to add it here - query = { - '_id.org': location.org, - '_id.course': location.course, - '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical']} - } + query = {'_id.org': location.org, + '_id.course': location.course, + '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical', + 'wrapper', 'problemset', 'conditional']} + } # we just want the Location, children, and inheritable metadata record_filter = {'_id': 1, 'definition.children': 1} @@ -282,6 +285,17 @@ class MongoModuleStore(ModuleStoreBase): # now go through the results and order them by the location url for result in resultset: location = Location(result['_id']) + # We need to collate between draft and non-draft + # i.e. draft verticals can have children which are not in non-draft versions + location = location._replace(revision=None) + location_url = location.url() + if location_url in results_by_url: + existing_children = results_by_url[location_url].get('definition', {}).get('children', []) + additional_children = result.get('definition', {}).get('children', []) + total_children = existing_children + additional_children + if 'definition' not in results_by_url[location_url]: + results_by_url[location_url]['definition'] = {} + results_by_url[location_url]['definition']['children'] = total_children results_by_url[location.url()] = result if location.category == 'course': root = location.url() @@ -293,17 +307,12 @@ class MongoModuleStore(ModuleStoreBase): """ Helper method for computing inherited metadata for a specific location url """ - my_metadata = {} # check for presence of metadata key. Note that a given module may not yet be fully formed. # example: update_item -> update_children -> update_metadata sequence on new item create # if we get called here without update_metadata called first then 'metadata' hasn't been set # as we're not fully transactional at the DB layer. Same comment applies to below key name # check my_metadata = results_by_url[url].get('metadata', {}) - for key in my_metadata.keys(): - if key not in INHERITABLE_METADATA: - del my_metadata[key] - results_by_url[url]['metadata'] = my_metadata # go through all the children and recurse, but only if we have # in the result set. Remember results will not contain leaf nodes diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 53eaebf850..97b3396baa 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -284,7 +284,7 @@ def import_from_xml(store, data_dir, course_dirs=None, except KeyError: # Ignore any missing keys in _model_data pass - + if 'data' in content: module_data = content['data'] @@ -301,16 +301,18 @@ def import_from_xml(store, data_dir, course_dirs=None, # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's # no good, so we have to do this kludge if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code - lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path, - static_content_store, link, remap_dict)) + lxml_rewrite_links(module_data, + lambda link: verify_content_links(module, course_data_path, static_content_store, link, remap_dict)) for key in remap_dict.keys(): module_data = module_data.replace(key, remap_dict[key]) - except Exception, e: + except Exception: logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location)) + else: + module_data = content - store.update_item(module.location, content) + store.update_item(module.location, module_data) if hasattr(module, 'children') and module.children != []: store.update_children(module.location, module.children) diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index 49b854e1d7..5075507bce 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -199,8 +199,8 @@ class PeerGradingModule(PeerGradingFields, XModule): self.student_data_for_location = response score_dict = { - 'score': int(count_graded >= count_required), - 'total': self.max_grade, + 'score': int(count_graded >= count_required and count_graded>0) * int(self.weight), + 'total': self.max_grade * int(self.weight), } return score_dict diff --git a/common/lib/xmodule/xmodule/tests/test_date_utils.py b/common/lib/xmodule/xmodule/tests/test_date_utils.py index 2b294e028f..af96de018f 100644 --- a/common/lib/xmodule/xmodule/tests/test_date_utils.py +++ b/common/lib/xmodule/xmodule/tests/test_date_utils.py @@ -5,6 +5,7 @@ from xmodule.util import date_utils import datetime import time + def test_get_time_struct_display(): assert_equals("", date_utils.get_time_struct_display(None, "")) test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0)) @@ -15,12 +16,20 @@ def test_get_time_struct_display(): def test_get_default_time_display(): assert_equals("", date_utils.get_default_time_display(None)) test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0)) - assert_equals("Mar 12, 1992 at 03:03 PM", + assert_equals( + "Mar 12, 1992 at 15:03 UTC", date_utils.get_default_time_display(test_time)) + assert_equals( + "Mar 12, 1992 at 15:03 UTC", + date_utils.get_default_time_display(test_time, True)) + assert_equals( + "Mar 12, 1992 at 15:03", + date_utils.get_default_time_display(test_time, False)) def test_time_to_datetime(): assert_equals(None, date_utils.time_to_datetime(None)) test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0)) - assert_equals(datetime.datetime(1992, 3, 12, 15, 3, 30), + assert_equals( + datetime.datetime(1992, 3, 12, 15, 3, 30), date_utils.time_to_datetime(test_time)) diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py index 443014f9ef..170a89d783 100644 --- a/common/lib/xmodule/xmodule/tests/test_export.py +++ b/common/lib/xmodule/xmodule/tests/test_export.py @@ -24,6 +24,11 @@ def strip_filenames(descriptor): """ print "strip filename from {desc}".format(desc=descriptor.location.url()) descriptor._model_data.pop('filename', None) + + if hasattr(descriptor, 'xml_attributes'): + if 'filename' in descriptor.xml_attributes: + del descriptor.xml_attributes['filename'] + for d in descriptor.get_children(): strip_filenames(d) diff --git a/common/lib/xmodule/xmodule/util/date_utils.py b/common/lib/xmodule/xmodule/util/date_utils.py index 3db44b3ee6..1e64856e8f 100644 --- a/common/lib/xmodule/xmodule/util/date_utils.py +++ b/common/lib/xmodule/xmodule/util/date_utils.py @@ -2,15 +2,18 @@ import time import datetime -def get_default_time_display(time_struct): +def get_default_time_display(time_struct, show_timezone=True): """ Converts a time struct to a string representation. This is the default representation used in Studio and LMS. - It is of the form "Apr 09, 2013 at 04:00 PM". + It is of the form "Apr 09, 2013 at 16:00" or "Apr 09, 2013 at 16:00 UTC", + depending on the value of show_timezone. - If None is passed in, an empty string will be returned. + If None is passed in for time_struct, an empty string will be returned. + The default value of show_timezone is True. """ - return get_time_struct_display(time_struct, "%b %d, %Y at %I:%M %p") + timezone = "" if time_struct is None or not show_timezone else " UTC" + return get_time_struct_display(time_struct, "%b %d, %Y at %H:%M") + timezone def get_time_struct_display(time_struct, format): diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index e5c1708ac4..c8a1ac009c 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -102,6 +102,7 @@ class XmlDescriptor(XModuleDescriptor): 'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc', 'ispublic', # if True, then course is listed for all users; see 'xqa_key', # for xqaa server access + 'giturl', # url of git server for origin of file # information about testcenter exams is a dict (of dicts), not a string, # so it cannot be easily exportable as a course element's attribute. 'testcenter_info', @@ -222,6 +223,7 @@ class XmlDescriptor(XModuleDescriptor): definition, children = cls.definition_from_xml(definition_xml, system) if definition_metadata: definition['definition_metadata'] = definition_metadata + definition['filename'] = [ filepath, filename ] return definition, children @@ -315,6 +317,7 @@ class XmlDescriptor(XModuleDescriptor): model_data['children'] = children model_data['xml_attributes'] = {} + model_data['xml_attributes']['filename'] = definition.get('filename', ['', None]) # for git link for key, value in metadata.items(): if key not in set(f.name for f in cls.fields + cls.lms.fields): model_data['xml_attributes'][key] = value diff --git a/common/static/js/vendor/timepicker/datepair.js b/common/static/js/vendor/timepicker/datepair.js index f210933593..cd1955ba04 100644 --- a/common/static/js/vendor/timepicker/datepair.js +++ b/common/static/js/vendor/timepicker/datepair.js @@ -24,7 +24,7 @@ $(function() { $('.datepair input.time').each(function() { var $this = $(this); - var opts = { 'showDuration': true, 'timeFormat': 'g:ia', 'scrollDefaultNow': true }; + var opts = { 'showDuration': true, 'timeFormat': 'H:i', 'scrollDefaultNow': true }; if ($this.hasClass('start') || $this.hasClass('end')) { opts.onSelect = doDatepair; diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index c1dd5b7f2d..493555b879 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -11,11 +11,15 @@ font-size: ($sizeValue/10) + rem; } +// ==================== + // line-height @function lh($amount: 1) { @return $body-line-height * $amount; } +// ==================== + // image-replacement hidden text @mixin text-hide() { text-indent: 100%; @@ -35,6 +39,8 @@ width: 1px; } +// ==================== + // vertical and horizontal centering @mixin vertically-and-horizontally-centered ($height, $width) { left: 50%; @@ -46,6 +52,8 @@ top: 150px; } +// ==================== + // sizing @mixin size($width: $baseline, $height: $baseline) { height: $height; @@ -56,6 +64,8 @@ @include size($size); } +// ==================== + // placeholder styling @mixin placeholder($color) { :-moz-placeholder { diff --git a/common/test/data/simple/course.xml b/common/test/data/simple/course.xml index 660411384f..b86a1898f9 100644 --- a/common/test/data/simple/course.xml +++ b/common/test/data/simple/course.xml @@ -12,13 +12,20 @@ - +
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index ae15b40d26..714e45842f 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -12,7 +12,6 @@ from django.contrib.auth.decorators import login_required from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import redirect from mitxmako.shortcuts import render_to_response, render_to_string -#from django.views.decorators.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control @@ -67,9 +66,9 @@ def user_groups(user): @ensure_csrf_cookie @cache_if_anonymous def courses(request): - ''' + """ Render "find courses" page. The course selection work is done in courseware.courses. - ''' + """ courses = get_courses(request.user, request.META.get('HTTP_HOST')) courses = sort_by_announcement(courses) @@ -77,14 +76,16 @@ def courses(request): def render_accordion(request, course, chapter, section, model_data_cache): - ''' Draws navigation bar. Takes current position in accordion as - parameter. + """ + Draws navigation bar. Takes current position in accordion as + parameter. - If chapter and section are '' or None, renders a default accordion. + If chapter and section are '' or None, renders a default accordion. - course, chapter, and section are the url_names. + course, chapter, and section are the url_names. - Returns the html string''' + Returns the html string + """ # grab the table of contents user = User.objects.prefetch_related("groups").get(id=request.user.id) @@ -92,7 +93,8 @@ def render_accordion(request, course, chapter, section, model_data_cache): context = dict([('toc', toc), ('course_id', course.id), - ('csrf', csrf(request)['csrf_token'])] + template_imports.items()) + ('csrf', csrf(request)['csrf_token']), + ('show_timezone', course.show_timezone)] + template_imports.items()) return render_to_string('courseware/accordion.html', context) @@ -166,10 +168,10 @@ def save_child_position(seq_module, child_name): def check_for_active_timelimit_module(request, course_id, course): - ''' + """ Looks for a timing module for the given user and course that is currently active. If found, returns a context dict with timer-related values to enable display of time remaining. - ''' + """ context = {} # TODO (cpennington): Once we can query the course structure, replace this with such a query @@ -201,11 +203,11 @@ def check_for_active_timelimit_module(request, course_id, course): def update_timelimit_module(user, course_id, model_data_cache, timelimit_descriptor, timelimit_module): - ''' + """ Updates the state of the provided timing module, starting it if it hasn't begun. Returns dict with timer-related values to enable display of time remaining. Returns 'timer_expiration_duration' in dict if timer is still active, and not if timer has expired. - ''' + """ context = {} # determine where to go when the exam ends: if timelimit_descriptor.time_expired_redirect_url is None: @@ -391,14 +393,14 @@ def index(request, course_id, chapter=None, section=None, @ensure_csrf_cookie def jump_to(request, course_id, location): - ''' + """ Show the page that contains a specific location. If the location is invalid or not in any class, return a 404. Otherwise, delegates to the index view to figure out whether this user has access, and what they should see. - ''' + """ # Complain if the location isn't valid try: location = Location(location) @@ -486,7 +488,9 @@ def syllabus(request, course_id): def registered_for_course(course, user): - '''Return CourseEnrollment if user is registered for course, else False''' + """ + Return CourseEnrollment if user is registered for course, else False + """ if user is None: return False if user.is_authenticated(): diff --git a/lms/djangoapps/instructor/management/commands/dump_grades.py b/lms/djangoapps/instructor/management/commands/dump_grades.py index 13f86c0e0f..3707ad33ed 100644 --- a/lms/djangoapps/instructor/management/commands/dump_grades.py +++ b/lms/djangoapps/instructor/management/commands/dump_grades.py @@ -3,17 +3,12 @@ # django management command: dump grades to csv files # for use by batch processes -import os -import sys -import string -import datetime -import json +import csv -from instructor.views import * +from instructor.views import get_student_grade_summary_data from courseware.courses import get_course_by_id from xmodule.modulestore.django import modulestore -from django.conf import settings from django.core.management.base import BaseCommand @@ -45,7 +40,7 @@ class Command(BaseCommand): request = self.DummyRequest() try: course = get_course_by_id(course_id) - except Exception as err: + except Exception: if course_id in modulestore().courses: course = modulestore().courses[course_id] else: diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 00e5810800..a3b4f42bf7 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -11,7 +11,6 @@ import requests from requests.status_codes import codes import urllib from collections import OrderedDict -import json from StringIO import StringIO @@ -21,7 +20,6 @@ from django.http import HttpResponse from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from mitxmako.shortcuts import render_to_response -import requests from django.core.urlresolvers import reverse from courseware import grades @@ -36,11 +34,7 @@ from django_comment_client.models import (Role, from django_comment_client.utils import has_forum_access from psychometrics import psychoanalyze from student.models import CourseEnrollment, CourseEnrollmentAllowed -from xmodule.course_module import CourseDescriptor -from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem -from xmodule.modulestore.search import path_to_location import xmodule.graders as xmgraders import track.views @@ -48,14 +42,15 @@ from .offline_gradecalc import student_grades, offline_grades_available log = logging.getLogger(__name__) -template_imports = {'urllib': urllib} - # internal commands for managing forum roles: FORUM_ROLE_ADD = 'add' FORUM_ROLE_REMOVE = 'remove' def split_by_comma_and_whitespace(s): + """ + Return string s, split by , or whitespace + """ return re.split(r'[\s,]', s) @@ -93,13 +88,13 @@ def instructor_dashboard(request, course_id): data += compute_course_stats(course).items() if request.user.is_staff: for field in course.fields: - if getattr(field.scope, 'student', False): + if getattr(field.scope, 'user', False): continue data.append([field.name, json.dumps(field.read_json(course))]) for namespace in course.namespaces: for field in getattr(course, namespace).fields: - if getattr(field.scope, 'student', False): + if getattr(field.scope, 'user', False): continue data.append(["{}.{}".format(namespace, field.name), json.dumps(field.read_json(course))]) @@ -141,7 +136,7 @@ def instructor_dashboard(request, course_id): # 'beta', so adding it to get_access_group_name doesn't really make # sense. name = course_beta_test_group_name(course.location) - (group, created) = Group.objects.get_or_create(name=name) + (group, _) = Group.objects.get_or_create(name=name) return group # process actions from form POST @@ -237,13 +232,13 @@ def instructor_dashboard(request, course_id): if '/' not in problem_to_reset: # allow state of modules other than problem to be reset problem_to_reset = "problem/" + problem_to_reset # but problem is the default try: - (org, course_name, run) = course_id.split("/") + (org, course_name, _) = course_id.split("/") module_state_key = "i4x://" + org + "/" + course_name + "/" + problem_to_reset module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id, course_id=course_id, module_state_key=module_state_key) msg += "Found module to reset. " - except Exception as e: + except Exception: msg += "Couldn't find module with that urlname. " if "Delete student state for problem" in action: @@ -352,7 +347,7 @@ def instructor_dashboard(request, course_id): return_csv('', datatable, fp=fp) fp.seek(0) files = {'datafile': fp} - msg2, dataset = _do_remote_gradebook(request.user, course, 'post-grades', files=files) + msg2, _ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) msg += msg2 @@ -423,7 +418,7 @@ def instructor_dashboard(request, course_id): datatable = {'header': ['username', 'email'] + profkeys} def getdat(u): p = u.profile - return [u.username, u.email] + [getattr(p,x,'') for x in profkeys] + return [u.username, u.email] + [getattr(p, x, '') for x in profkeys] datatable['data'] = [getdat(u) for u in enrolled_students] datatable['title'] = 'Student profile data for course %s' % course_id @@ -433,17 +428,17 @@ def instructor_dashboard(request, course_id): elif 'Download CSV of all responses to problem' in action: problem_to_dump = request.POST.get('problem_to_dump','') - if problem_to_dump[-4:]==".xml": - problem_to_dump=problem_to_dump[:-4] + if problem_to_dump[-4:] == ".xml": + problem_to_dump = problem_to_dump[:-4] try: - (org, course_name, run)=course_id.split("/") - module_state_key="i4x://"+org+"/"+course_name+"/problem/"+problem_to_dump + (org, course_name, run) = course_id.split("/") + module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_dump smdat = StudentModule.objects.filter(course_id=course_id, module_state_key=module_state_key) smdat = smdat.order_by('student') msg += "Found %d records to dump " % len(smdat) except Exception as err: - msg+="Couldn't find module with that urlname. " + msg += "Couldn't find module with that urlname. " msg += "
%s
" % escape(err) smdat = [] @@ -741,7 +736,7 @@ def _list_course_forum_members(course_id, rolename, datatable): # make sure datatable is set up properly for display first, before checking for errors datatable['header'] = ['Username', 'Full name', 'Roles'] datatable['title'] = 'List of Forum {0}s in course {1}'.format(rolename, course_id) - datatable['data'] = []; + datatable['data'] = [] try: role = Role.objects.get(name=rolename, course_id=course_id) except Role.DoesNotExist: @@ -923,7 +918,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, datarow = [student.id, student.username, student.profile.name, student.email] try: datarow.append(student.externalauthmap.external_email) - except: # ExternalAuthMap.DoesNotExist + except: # ExternalAuthMap.DoesNotExist datarow.append('') if get_grades: @@ -1040,7 +1035,8 @@ def _do_enroll_students(course, course_id, students, overload=False): datatable['data'] = [[x, status[x]] for x in status] datatable['title'] = 'Enrollment of students' - def sf(stat): return [x for x in status if status[x] == stat] + def sf(stat): + return [x for x in status if status[x] == stat] data = dict(added=sf('added'), rejected=sf('rejected') + sf('exists'), deleted=sf('deleted'), datatable=datatable) @@ -1136,7 +1132,7 @@ def dump_grading_context(course): ''' msg = "-----------------------------------------------------------------------------\n" msg += "Course grader:\n" - + msg += '%s\n' % course.grader.__class__ graders = {} if isinstance(course.grader, xmgraders.WeightedSubsectionsGrader): @@ -1151,13 +1147,13 @@ def dump_grading_context(course): gc = course.grading_context msg += "graded sections:\n" - + msg += '%s\n' % gc['graded_sections'].keys() for (gs, gsvals) in gc['graded_sections'].items(): msg += "--> Section %s:\n" % (gs) for sec in gsvals: s = sec['section_descriptor'] - format = getattr(s, 'format', None) + format = getattr(s.lms, 'format', None) aname = '' if format in graders: g = graders[format] diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py index 78da00bf2b..cb617d609d 100644 --- a/lms/djangoapps/open_ended_grading/views.py +++ b/lms/djangoapps/open_ended_grading/views.py @@ -27,8 +27,6 @@ from mitxmako.shortcuts import render_to_string log = logging.getLogger(__name__) -template_imports = {'urllib': urllib} - system = ModuleSystem( ajax_url=None, track_function=None, diff --git a/lms/static/images/university/delft/delft-cover.jpg b/lms/static/images/university/delft/delft-cover.jpg index e69c836908..fe41928b41 100644 Binary files a/lms/static/images/university/delft/delft-cover.jpg and b/lms/static/images/university/delft/delft-cover.jpg differ diff --git a/lms/templates/courseware/accordion.html b/lms/templates/courseware/accordion.html index da774a4486..157e6e1bec 100644 --- a/lms/templates/courseware/accordion.html +++ b/lms/templates/courseware/accordion.html @@ -11,7 +11,7 @@
  • ${section['display_name']}

    -

    ${section['format']} ${"due " + get_default_time_display(section['due']) if section.get('due') is not None else ''}

    +

    ${section['format']} ${"due " + get_default_time_display(section['due'], show_timezone) if section.get('due') is not None else ''}

  • % endfor diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index 9e2a2e5982..fcd4348f96 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -64,7 +64,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", %if section.get('due') is not None: - due ${get_default_time_display(section['due'])} + due ${get_default_time_display(section['due'], course.show_timezone)} %endif

    diff --git a/lms/templates/staff_problem_info.html b/lms/templates/staff_problem_info.html index 807182b059..7b4abf13fd 100644 --- a/lms/templates/staff_problem_info.html +++ b/lms/templates/staff_problem_info.html @@ -1,15 +1,17 @@ ## The JS for this is defined in xqa_interface.html ${module_content} -%if location.category in ['problem','video','html']: +%if location.category in ['problem','video','html','combinedopenended']: % if edit_link:
    - Edit / - Edit + % if xqa_key: + / QA + % endif
    % endif @@ -61,6 +63,12 @@ location = ${location | h} ${name}
    ${field | h}
    %endfor + + + %for name, field in xml_attributes.items(): + + %endfor +
    XML attributes
    ${name}
    ${field | h}
    category = ${category | h}
    %if render_histogram: diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html index afc1526dc7..7d57abb47e 100644 --- a/lms/templates/static_templates/jobs.html +++ b/lms/templates/static_templates/jobs.html @@ -75,8 +75,8 @@
    -

    DIRECTOR OF EDUCATIONAL SERVICES

    -

    The edX Director of Education Services reporting to the VP of Engineering and Educational Services is responsible for:

    +

    VICE PRESIDENT/DIRECTOR OF EDUCATIONAL SERVICES

    +

    The edX VP/Director of Education Services reporting to the VP of Engineering and Educational Services is responsible for:

    1. Delivering 20 new courses in 2013 in collaboration with the partner Universities
        @@ -419,13 +419,76 @@
    +
    +
    +

    SALES ENGINEER, BUSINESS DEVELOPMENT TEAM

    +

    A great relationship with edX begins long before the first student signs up. We are +looking for some talented, self-motivated people to help set a solid foundation for +our emerging corporate customers, NGO’s, and governmental partners. As the Sales +Engineer you will be expected to provide oversight if requested over more junior +staff. This may include skills development, knowledge transfer, sharing technical +expertise, and review of demos prior to presentation. The Sales Engineer should +have familiarity with instructional design and competency in that area is a plus.

    + +

    Experience teaching and mentoring, needs assessment and prior management +responsibility, and LMS experience, also a plus. This is a team atmosphere with +many constituencies working to develop a new global perspective regarding higher +education and online learning. Respect and patience, along with knowledge and +understanding of the development process is critical to success in order to maintain +strong bonds between development teams, sales team, and prospect/client +implementation teams. In addition the Sales Engineer may also work with our +xUniversity partners and the affiliated professors joining the edX movement. This +position requires customer facing skills, comfort in demonstrating the product, and +ability to code ‘demos’ as required. Additionally you will be contributing to +proposals, so clear documentation and writing skills are critical. The job will +require travel to client sites around the US upon occasion, and possibly +internationally as well. Job also requires good speaking skills, and a willingness and +ability to communicate clearly and respond quickly to prospect and customer +requests. This is a salaried position and will on occasion require work and +responsiveness to both the edX team and customers ‘after hours’. This position +reports to the VP, Business Development and will be dotted lined to the +development and program management teams.

    +

    Responsibilities:

    +
      +
    • Can code demos and evaluate demos of others
    • +
    • Prepare and deliver standard and custom demonstrations
    • +
    • Handle all pre-sales technical issues professionally and efficiently
    • +
    • Maintain in-depth knowledge of products and pending new releases
    • +
    • Maintain a working knowledge of documentation and training
    • +
    • Maintain a working knowledge of workflow systems
    • +
    • Respond to technical questions from universities looking to expand their on-line offerings
    • +
    • Provide feedback to Product Development regarding new features, improving product performance, and eliminating bugs in the product
    • +
    • Prepare Professional Services for efficient onboarding – professionally managing the transition from pre-sales to post-sales
    • +
    • Deliver high-level presentation and associated ‘click-thru’ demonstrations
    • +
    • and be able to customize to prospect’s requirements
    • +
    • Understand and articulate the underlying technology concepts
    • +
    • Understand and articulate how all products components fit together technically as well as how they integrate and work with external technologies and cross functional applications found within clients organizations.
    • +
    • Build relationships with our prospects and universities, to be viewed as a trusted training partner.
    • +
    +

    Qualifications:

    +
      +
    • Minimum of 5 years of experience working closely with relationship based sales organizations, preferably in an educational technology organization.
    • +
    • Excellent interpersonal skills including proven presentation and facilitation skills.
    • +
    • Strong oral and written communication skills.
    • +
    • Flexibility to work on a variety of initiatives; prior startup experience preferred.
    • +
    • Outstanding work ethic, results-oriented, and creative/innovative style.
    • +
    • Proactive, optimistic approach to problem solving.
    • +
    • Commitment to constant personal and organizational improvement.
    • +
    • Willingness to travel to partner sites as needed.
    • +
    • Lean and Agile thinking and training. Experienced in Scrum or kanban.
    • +
    • Bachelors or Master’s in Education, organizational learning, or other related field preferred. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
    • +
    + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +

    Positions

    How to Apply

    E-mail your resume, cover letter and any other materials to jobs@edx.org

    diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py index 3ff1ed3971..14a6049186 100644 --- a/lms/xmodule_namespace.py +++ b/lms/xmodule_namespace.py @@ -52,7 +52,8 @@ class LmsNamespace(Namespace): start = Date(help="Start time when this module is visible", scope=Scope.settings) due = Date(help="Date that this problem is due by", scope=Scope.settings) - source_file = String(help="DO NOT USE", scope=Scope.settings) + source_file = String(help="source file name (eg for latex)", scope=Scope.settings) + giturl = String(help="url root for course data git repository", scope=Scope.settings) xqa_key = String(help="DO NOT USE", scope=Scope.settings) ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings) graceperiod = Timedelta(