diff --git a/AUTHORS b/AUTHORS index 9bb4ede121..03959ca00d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -78,3 +78,4 @@ Peter Fogg Bethany LaPenta Renzo Lucioni Felix Sun +Adam Palay diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ff900d6161..3011821caf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Common: Student information is now passed to the tracking log via POST instead of GET. + Common: Add tests for documentation generation to test suite Blades: User answer now preserved (and changeable) after clicking "show answer" in choice problems @@ -13,6 +15,8 @@ LMS: Users are no longer auto-activated if they click "reset password" This is now done when they click on the link in the reset password email they receive (along with usual path through activation email). +LMS: Fixed a reflected XSS problem in the static textbook views. + LMS: Problem rescoring. Added options on the Grades tab of the Instructor Dashboard to allow a particular student's submission for a particular problem to be rescored. Provides an option to see a diff --git a/README.md b/README.md index 92a4116354..4dbf069da3 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,12 @@ otherwise noted. Please see ``LICENSE.txt`` for details. +Documentation +------------ + +High-level documentation of the code is located in the `doc` subdirectory. Start +with `overview.md` to get an introduction to the architecture of the system. + How to Contribute ----------------- diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py index 9552d35036..fe20fb9b77 100644 --- a/cms/djangoapps/contentstore/features/checklists.py +++ b/cms/djangoapps/contentstore/features/checklists.py @@ -115,7 +115,7 @@ def clickActionLink(checklist, task, actionText): # text will be empty initially, wait for it to populate def verify_action_link_text(driver): - return action_link.text == actionText + return world.css_text('#course-checklist' + str(checklist) + ' a', index=task) == actionText world.wait_for(verify_action_link_text) - action_link.click() + world.css_click('#course-checklist' + str(checklist) + ' a', index=task) diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature new file mode 100644 index 0000000000..2291712f2d --- /dev/null +++ b/cms/djangoapps/contentstore/features/component.feature @@ -0,0 +1,69 @@ +Feature: Component Adding + As a course author, I want to be able to add a wide variety of components + + @skip + Scenario: I can add components + Given I have opened a new course in studio + And I am editing a new unit + When I add the following components: + | Component | + | Discussion | + | Blank HTML | + | LaTex | + | Blank Problem| + | Dropdown | + | Multi Choice | + | Numerical | + | Text Input | + | Advanced | + | Circuit | + | Custom Python| + | Image Mapped | + | Math Input | + | Problem LaTex| + | Adaptive Hint| + | Video | + Then I see the following components: + | Component | + | Discussion | + | Blank HTML | + | LaTex | + | Blank Problem| + | Dropdown | + | Multi Choice | + | Numerical | + | Text Input | + | Advanced | + | Circuit | + | Custom Python| + | Image Mapped | + | Math Input | + | Problem LaTex| + | Adaptive Hint| + | Video | + + @skip + Scenario: I can delete Components + Given I have opened a new course in studio + And I am editing a new unit + And I add the following components: + | Component | + | Discussion | + | Blank HTML | + | LaTex | + | Blank Problem| + | Dropdown | + | Multi Choice | + | Numerical | + | Text Input | + | Advanced | + | Circuit | + | Custom Python| + | Image Mapped | + | Math Input | + | Problem LaTex| + | Adaptive Hint| + | Video | + When I will confirm all alerts + And I delete all components + Then I see no components diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py new file mode 100644 index 0000000000..217ad84591 --- /dev/null +++ b/cms/djangoapps/contentstore/features/component.py @@ -0,0 +1,126 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from nose.tools import assert_true + +DATA_LOCATION = 'i4x://edx/templates' + + +@step(u'I am editing a new unit') +def add_unit(step): + css_selectors = ['a.new-courseware-section-button', 'input.new-section-name-save', 'a.new-subsection-item', + 'input.new-subsection-name-save', 'div.section-item a.expand-collapse-icon', 'a.new-unit-item'] + for selector in css_selectors: + world.css_click(selector) + + +@step(u'I add the following components:') +def add_components(step): + for component in [step_hash['Component'] for step_hash in step.hashes]: + assert component in COMPONENT_DICTIONARY + for css in COMPONENT_DICTIONARY[component]['steps']: + world.css_click(css) + + +@step(u'I see the following components') +def check_components(step): + for component in [step_hash['Component'] for step_hash in step.hashes]: + assert component in COMPONENT_DICTIONARY + assert_true(COMPONENT_DICTIONARY[component]['found_func'](), "{} couldn't be found".format(component)) + + +@step(u'I delete all components') +def delete_all_components(step): + for _ in range(len(COMPONENT_DICTIONARY)): + world.css_click('a.delete-button') + + +@step(u'I see no components') +def see_no_components(steps): + assert world.is_css_not_present('li.component') + + +def step_selector_list(data_type, path, index=1): + selector_list = ['a[data-type="{}"]'.format(data_type)] + if index != 1: + selector_list.append('a[id="ui-id-{}"]'.format(index)) + if path is not None: + selector_list.append('a[data-location="{}/{}/{}"]'.format(DATA_LOCATION, data_type, path)) + return selector_list + + +def found_text_func(text): + return lambda: world.browser.is_text_present(text) + + +def found_css_func(css): + return lambda: world.is_css_present(css, wait_time=2) + +COMPONENT_DICTIONARY = { + 'Discussion': { + 'steps': step_selector_list('discussion', None), + 'found_func': found_css_func('section.xmodule_DiscussionModule') + }, + 'Blank HTML': { + 'steps': step_selector_list('html', 'Blank_HTML_Page'), + #this one is a blank html so a more refined search is being done + 'found_func': lambda: '\n \n' in [x.html for x in world.css_find('section.xmodule_HtmlModule')] + }, + 'LaTex': { + 'steps': step_selector_list('html', 'E-text_Written_in_LaTeX'), + 'found_func': found_text_func('EXAMPLE: E-TEXT PAGE') + }, + 'Blank Problem': { + 'steps': step_selector_list('problem', 'Blank_Common_Problem'), + 'found_func': found_text_func('BLANK COMMON PROBLEM') + }, + 'Dropdown': { + 'steps': step_selector_list('problem', 'Dropdown'), + 'found_func': found_text_func('DROPDOWN') + }, + 'Multi Choice': { + 'steps': step_selector_list('problem', 'Multiple_Choice'), + 'found_func': found_text_func('MULTIPLE CHOICE') + }, + 'Numerical': { + 'steps': step_selector_list('problem', 'Numerical_Input'), + 'found_func': found_text_func('NUMERICAL INPUT') + }, + 'Text Input': { + 'steps': step_selector_list('problem', 'Text_Input'), + 'found_func': found_text_func('TEXT INPUT') + }, + 'Advanced': { + 'steps': step_selector_list('problem', 'Blank_Advanced_Problem', index=2), + 'found_func': found_text_func('BLANK ADVANCED PROBLEM') + }, + 'Circuit': { + 'steps': step_selector_list('problem', 'Circuit_Schematic_Builder', index=2), + 'found_func': found_text_func('CIRCUIT SCHEMATIC BUILDER') + }, + 'Custom Python': { + 'steps': step_selector_list('problem', 'Custom_Python-Evaluated_Input', index=2), + 'found_func': found_text_func('CUSTOM PYTHON-EVALUATED INPUT') + }, + 'Image Mapped': { + 'steps': step_selector_list('problem', 'Image_Mapped_Input', index=2), + 'found_func': found_text_func('IMAGE MAPPED INPUT') + }, + 'Math Input': { + 'steps': step_selector_list('problem', 'Math_Expression_Input', index=2), + 'found_func': found_text_func('MATH EXPRESSION INPUT') + }, + 'Problem LaTex': { + 'steps': step_selector_list('problem', 'Problem_Written_in_LaTeX', index=2), + 'found_func': found_text_func('PROBLEM WRITTEN IN LATEX') + }, + 'Adaptive Hint': { + 'steps': step_selector_list('problem', 'Problem_with_Adaptive_Hint', index=2), + 'found_func': found_text_func('PROBLEM WITH ADAPTIVE HINT') + }, + 'Video': { + 'steps': step_selector_list('video', None), + 'found_func': found_css_func('section.xmodule_VideoModule') + } +} diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index d838061698..e7fbb2f90c 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -60,8 +60,7 @@ def change_date(_step, new_date): @step(u'I should see the date "([^"]*)"$') def check_date(_step, date): date_css = 'span.date-display' - date_html = world.css_find(date_css) - assert date == date_html.html + assert date == world.css_html(date_css) @step(u'I modify the handout to "([^"]*)"$') @@ -74,8 +73,7 @@ def edit_handouts(_step, text): @step(u'I see the handout "([^"]*)"$') def check_handout(_step, handout): handout_css = 'div.handouts-content' - handouts = world.css_find(handout_css) - assert handout in handouts.html + assert handout in world.css_html(handout_css) def change_text(text): diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index 4e59897c1c..dc41cda30f 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -47,7 +47,7 @@ def confirm_change(step): range_css = '.range' all_ranges = world.css_find(range_css) for i in range(len(all_ranges)): - assert all_ranges[i].html != '0-50' + assert world.css_html(range_css, index=i) != '0-50' @step(u'I change assignment type "([^"]*)" to "([^"]*)"$') diff --git a/cms/djangoapps/contentstore/features/static-pages.py b/cms/djangoapps/contentstore/features/static-pages.py index a16a3246da..3c9226f874 100644 --- a/cms/djangoapps/contentstore/features/static-pages.py +++ b/cms/djangoapps/contentstore/features/static-pages.py @@ -9,14 +9,14 @@ from selenium.webdriver.common.keys import Keys def go_to_static(_step): menu_css = 'li.nav-course-courseware' static_css = 'li.nav-course-courseware-pages' - world.css_find(menu_css).click() - world.css_find(static_css).click() + world.css_click(menu_css) + world.css_click(static_css) @step(u'I add a new page') def add_page(_step): button_css = 'a.new-button' - world.css_find(button_css).click() + world.css_click(button_css) @step(u'I should( not)? see a "([^"]*)" static page$') @@ -33,13 +33,13 @@ def click_edit_delete(_step, edit_delete, page): button_css = 'a.%s-button' % edit_delete index = get_index(page) assert index != -1 - world.css_find(button_css)[index].click() + world.css_click(button_css, index=index) @step(u'I change the name to "([^"]*)"$') def change_name(_step, new_name): settings_css = '#settings-mode' - world.css_find(settings_css).click() + world.css_click(settings_css) input_css = 'input.setting-input' name_input = world.css_find(input_css) old_name = name_input.value @@ -47,13 +47,13 @@ def change_name(_step, new_name): name_input._element.send_keys(Keys.END, Keys.BACK_SPACE) name_input._element.send_keys(new_name) save_button = 'a.save-button' - world.css_find(save_button).click() + world.css_click(save_button) def get_index(name): page_name_css = 'section[data-type="HTMLModule"]' all_pages = world.css_find(page_name_css) for i in range(len(all_pages)): - if all_pages[i].html == '\n {name}\n'.format(name=name): + if world.css_html(page_name_css, index=i) == '\n {name}\n'.format(name=name): return i return -1 diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 47d770dc47..977bad8fea 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -16,14 +16,14 @@ HTTP_PREFIX = "http://localhost:8001" def go_to_uploads(_step): menu_css = 'li.nav-course-courseware' uploads_css = 'li.nav-course-courseware-uploads' - world.css_find(menu_css).click() - world.css_find(uploads_css).click() + world.css_click(menu_css) + world.css_click(uploads_css) @step(u'I upload the file "([^"]*)"$') def upload_file(_step, file_name): upload_css = 'a.upload-button' - world.css_find(upload_css).click() + world.css_click(upload_css) file_css = 'input.file-input' upload = world.css_find(file_css) @@ -32,7 +32,7 @@ def upload_file(_step, file_name): upload._element.send_keys(os.path.abspath(path)) close_css = 'a.close-button' - world.css_find(close_css).click() + world.css_click(close_css) @step(u'I should( not)? see the file "([^"]*)" was uploaded$') @@ -67,7 +67,7 @@ def no_duplicate(_step, file_name): all_names = world.css_find(names_css) only_one = False for i in range(len(all_names)): - if file_name == all_names[i].html: + if file_name == world.css_html(names_css, index=i): only_one = not only_one assert only_one @@ -100,7 +100,7 @@ def get_index(file_name): names_css = 'td.name-col > a.filename' all_names = world.css_find(names_css) for i in range(len(all_names)): - if file_name == all_names[i].html: + if file_name == world.css_html(names_css, index=i): return i return -1 diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b946aac6bb..a59394aa85 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -344,6 +344,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): err_cnt = perform_xlint('common/test/data', ['full']) self.assertGreater(err_cnt, 0) + @override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*']) + def test_module_preview_in_whitelist(self): + ''' + Tests the ajax callback to render an XModule + ''' + direct_store = modulestore('direct') + import_from_xml(direct_store, 'common/test/data/', ['full']) + + html_module_location = Location(['i4x', 'edX', 'full', 'html', 'html_90', None]) + + url = reverse('preview_component', kwargs={'location': html_module_location.url()}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertIn('Inline content', resp.content) + + # also try a custom response which will trigger the 'is this course in whitelist' logic + problem_module_location = Location(['i4x', 'edX', 'full', 'problem', 'H1P1_Energy', None]) + url = reverse('preview_component', kwargs={'location': problem_module_location.url()}) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + def test_delete(self): direct_store = modulestore('direct') import_from_xml(direct_store, 'common/test/data/', ['full']) diff --git a/cms/djangoapps/contentstore/tests/test_request_event.py b/cms/djangoapps/contentstore/tests/test_request_event.py new file mode 100644 index 0000000000..a235c71568 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_request_event.py @@ -0,0 +1,36 @@ +"""Tests for CMS's requests to logs""" +from django.test import TestCase +from django.core.urlresolvers import reverse +from contentstore.views.requests import event as cms_user_track + + +class CMSLogTest(TestCase): + """ + Tests that request to logs from CMS return 204s + """ + + def test_post_answers_to_log(self): + """ + Checks that student answer requests submitted to cms's "/event" url + via POST are correctly returned as 204s + """ + requests = [ + {"event": "my_event", "event_type": "my_event_type", "page": "my_page"}, + {"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"} + ] + for request_params in requests: + response = self.client.post(reverse(cms_user_track), request_params) + self.assertEqual(response.status_code, 204) + + def test_get_answers_to_log(self): + """ + Checks that student answer requests submitted to cms's "/event" url + via GET are correctly returned as 204s + """ + requests = [ + {"event": "my_event", "event_type": "my_event_type", "page": "my_page"}, + {"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"} + ] + for request_params in requests: + response = self.client.get(reverse(cms_user_track), request_params) + self.assertEqual(response.status_code, 204) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index deef6a27c9..fd2188a734 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -17,10 +17,13 @@ from xmodule.modulestore.mongo import MongoUsage from xmodule.x_module import ModuleSystem from xblock.runtime import DbModel +from util.sandboxing import can_execute_unsafe_code + import static_replace from .session_kv_store import SessionKeyValueStore from .requests import render_from_lms from .access import has_access +from ..utils import get_course_for_item __all__ = ['preview_dispatch', 'preview_component'] @@ -93,6 +96,8 @@ def preview_module_system(request, preview_id, descriptor): MongoUsage(preview_id, descriptor.location.url()), ) + course_id = get_course_for_item(descriptor.location).location.course_id + return ModuleSystem( ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'), # TODO (cpennington): Do we want to track how instructors are using the preview problems? @@ -104,6 +109,7 @@ def preview_module_system(request, preview_id, descriptor): replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location), user=request.user, xblock_model_data=preview_model_data, + can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), ) diff --git a/cms/envs/aws.py b/cms/envs/aws.py index c6a383211f..3b8baabc0a 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -105,6 +105,8 @@ ADMINS = ENV_TOKENS.get('ADMINS', ADMINS) SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL) MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS) +COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", []) + #Timezone overrides TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) diff --git a/cms/envs/test.py b/cms/envs/test.py index 8fe9652c07..86925caff6 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -140,3 +140,6 @@ SEGMENT_IO_KEY = '***REMOVED***' MITX_FEATURES['STUDIO_NPS_SURVEY'] = False MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True + +# Enabling SQL tracking logs for testing on common/djangoapps/track +MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True diff --git a/cms/static/img/edx-studio-large.png b/cms/static/img/edx-studio-large.png deleted file mode 100644 index d3ea3382a8..0000000000 Binary files a/cms/static/img/edx-studio-large.png and /dev/null differ diff --git a/cms/static/img/edx-studio-logo-small.png b/cms/static/img/edx-studio-logo-small.png deleted file mode 100644 index 728a3f81e0..0000000000 Binary files a/cms/static/img/edx-studio-logo-small.png and /dev/null differ diff --git a/cms/static/img/logo-edx-studio-white.png b/cms/static/img/logo-edx-studio-white.png deleted file mode 100644 index 3e3ee63622..0000000000 Binary files a/cms/static/img/logo-edx-studio-white.png and /dev/null differ diff --git a/cms/static/img/logo-edx-studio.png b/cms/static/img/logo-edx-studio.png index 006194a195..bad6af1697 100644 Binary files a/cms/static/img/logo-edx-studio.png and b/cms/static/img/logo-edx-studio.png differ diff --git a/cms/static/js/base.js b/cms/static/js/base.js index d1cffdc427..c2f401c77e 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -56,11 +56,11 @@ $(document).ready(function() { // nav - dropdown related $body.click(function(e) { - $('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown'); - $('.nav-dropdown .nav-item .title').removeClass('is-selected'); + $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); + $('.nav-dd .nav-item .title').removeClass('is-selected'); }); - $('.nav-dropdown .nav-item .title').click(function(e) { + $('.nav-dd .nav-item .title').click(function(e) { $subnav = $(this).parent().find('.wrapper-nav-sub'); $title = $(this).parent().find('.title'); @@ -71,8 +71,8 @@ $(document).ready(function() { $subnav.removeClass('is-shown'); $title.removeClass('is-selected'); } else { - $('.nav-dropdown .nav-item .title').removeClass('is-selected'); - $('.nav-dropdown .nav-item .wrapper-nav-sub').removeClass('is-shown'); + $('.nav-dd .nav-item .title').removeClass('is-selected'); + $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); $title.addClass('is-selected'); $subnav.addClass('is-shown'); } diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index bad87952d6..5994a8dbad 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -47,7 +47,7 @@ $gray-d2: shade($gray,40%); $gray-d3: shade($gray,60%); $gray-d4: shade($gray,80%); -$blue: rgb(85, 151, 221); +$blue: rgb(0, 159, 230); $blue-l1: tint($blue,20%); $blue-l2: tint($blue,40%); $blue-l3: tint($blue,60%); diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss index e0b4a004ec..8c43934a44 100644 --- a/cms/static/sass/elements/_controls.scss +++ b/cms/static/sass/elements/_controls.scss @@ -135,7 +135,48 @@ // ==================== -// layout-based buttons +// simple dropdown button styling - should we move this elsewhere? +.btn-dd { + @extend .btn; + @extend .btn-pill; + padding:($baseline/4) ($baseline/2); + border-width: 1px; + border-style: solid; + border-color: transparent; + text-align: center; + + &:hover, &:active { + @extend .fake-link; + border-color: $gray-l3; + } + + &.current, &.active, &.is-selected { + @include box-shadow(inset 0 1px 2px 1px $shadow-l1); + border-color: $gray-l3; + } +} + +// layout-based buttons - nav dd +.btn-dd-nav-primary { + @extend .btn-dd; + background: $white; + border-color: $white; + color: $gray-d1; + + &:hover, &:active { + background: $white; + color: $blue-s1; + } + + &.current, &.active { + background: $white; + color: $gray-d4; + + &:hover, &:active { + color: $blue-s1; + } + } +} // ==================== diff --git a/cms/static/sass/elements/_header.scss b/cms/static/sass/elements/_header.scss index 247bb35b81..026ca96274 100644 --- a/cms/static/sass/elements/_header.scss +++ b/cms/static/sass/elements/_header.scss @@ -3,448 +3,310 @@ .wrapper-header { @extend .depth3; - margin: 0; - padding: $baseline; - border-bottom: 1px solid $gray; - @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.2)); - background: $white; - height: 76px; + @include box-shadow(0 1px 2px 0 $shadow-l1); position: relative; width: 100%; - - a { - color: $baseFontColor; - display: block; - - &:hover, &:active { - color: $blue; - } - } + margin: 0; + padding: 0 $baseline; + background: $white; header.primary { + @include box-sizing(border-box); @include clearfix(); max-width: $fg-max-width; min-width: $fg-min-width; width: flex-grid(12); margin: 0 auto; - color: $gray-l1; } - nav .nav-item { - display: inline-block; - } -} + // ==================== -// ==================== + // basic layout -// basic layout -.wrapper-left, .wrapper-right { - @include box-sizing(border-box); -} - -.wrapper-left { - width: flex-grid(10, 12); - float: left; - margin-right: flex-gutter(); -} - -.wrapper-right { - width: flex-grid(2, 12); - float: right; -} - -// ==================== - -// specific elements - branding -.branding, .info-course, .nav-course, .nav-account, .nav-unauth, .nav-pitch { - display: inline-block; - vertical-align: top; -} - -.branding { - position: relative; - margin: 0 ($baseline/2) 0 0; - padding-right: ($baseline*0.75); - - a { - @extend .text-hide; - display: block; - width: 164px; - height: 32px; - background: transparent url('../img/logo-edx-studio.png') 0 0 no-repeat; - } -} - -// ==================== - -// specific elements - course name/info -.info-course { - @include font-size(14); - position: relative; - margin: -3px ($baseline/2) 0 0; - padding-right: ($baseline*0.75); - - &:before { - @extend .faded-vertical-divider; - content: ""; - display: block; - height: 50px; - position: absolute; - right: 1px; - top: -8px; - width: 1px; - } - - &:after { - @extend .faded-vertical-divider-light; - content: ""; - display: block; - height: 50px; - position: absolute; - right: 0px; - top: -12px; - width: 1px; - } - - .course-number, .course-org { - @include font-size(12); - display: inline-block; - max-width: 70px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - line-height: 1.3; - } - - .course-org { - margin-right: ($baseline/4); - max-width: 140px; - } - - .course-title { - display: block; - width: 100%; - max-width: 220px; - overflow: hidden; - margin-top: -4px; - white-space: nowrap; - text-overflow: ellipsis; - @include font-size(16); - font-weight: 600; - } -} - -// ==================== - -// specific elements - course nav -.nav-course { - width: 290px; - @extend .t-copy-sub1; - margin-top: -($baseline/4); - - > ol > .nav-item { - vertical-align: bottom; - margin: 0 ($baseline/2) 0 0; - - &:last-child { - margin-right: 0; - } - - .title { - display: block; - padding: 0 ($baseline/4); - text-transform: uppercase; - font-weight: 600; - color: $gray-d3; - - .label-prefix { - @include font-size(11); - display: block; - font-weight: 400; - } - } - - // specific nav items - &.nav-course-courseware { - } - - &.nav-course-settings { - } - - &.nav-course-tools { - } - } -} - -// ==================== - -// specific elements - account-based nav -.nav-account { - width: 100%; - margin-top: ($baseline*0.75); - @include font-size(14); - text-align: right; - - .nav-account-username { - width: 100%; - - .icon-user { - display: inline-block; - vertical-align: middle; - margin-right: 3px; - @include font-size(12); - } - - .account-username { - display: inline-block; - vertical-align: middle; - width: 80%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .icon-expand { - display: inline-block; - vertical-align: middle; - } - } -} - -// ==================== - -// UI - dropdown -.nav-dropdown { - - .nav-item { - position: relative; - - .icon-caret-down { - @include font-size(14); - @include transition (color 0.5s ease-in-out, opacity 0.5s ease-in-out); - display: inline-block; - vertical-align: middle; - margin-left: 2px; - opacity: 0.5; - color: $gray-l2; - } - - &:hover { - - .icon-caret-down { - color: $blue; - opacity: 1.0; - } - } - } - - .wrapper-nav-sub { - position: absolute; - left: -7px; - top: 47px; - width: 140px; - opacity: 0.0; - pointer-events: none; - } - - .nav-sub { - @include border-radius(2px); - @include box-sizing(border-box); - @include box-shadow(0 1px 5px 0 rgba(0,0,0, 0.1)); - position: relative; - width: 100%; - border: 1px solid $gray-l2; - padding: ($baseline/4) ($baseline/2); + .wrapper-l, .wrapper-r { background: $white; - - &:after, &:before { - bottom: 100%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - } - - &:after { - border-color: rgba(255, 255, 255, 0); - border-bottom-color: #fff; - border-width: 5px; - right: 3px; - margin-left: -5px; - } - - &:before { - border-color: rgba(178, 178, 178, 0); - border-bottom-color: $gray-l2; - border-width: 6px; - right: 3px; - margin-left: -6px; - } - - .nav-item { - display: block; - margin: 0 0 ($baseline/4) 0; - border-bottom: 1px solid $gray-l5; - padding: 0 0($baseline/4) 0; - @include font-size(13); - - &:last-child { - margin-bottom: 0; - border-bottom: none; - padding-bottom: 0; - } - - a { - display: block; - } - } } - // UI - dropdown - specific navs - &.nav-account { - - .wrapper-nav-sub { - top: 27px; - left: auto; - right: -13px; - width: 110px; - } - - .nav-sub { - text-align: left; - - .icon-expand { - top: -2px; - } - } - - .nav-sub:after { - left: auto; - right: 11px; - } - - .nav-sub:before { - left: auto; - right: 10px; - } + .wrapper-l { + float: left; + width: flex-grid(7,12); } - &.nav-course { - - .nav-course-courseware { - - .nav-sub:after { - left: 88px; - } - - .nav-sub:before { - left: 88px; - } - } - - .nav-course-settings { - - .nav-sub:after { - left: 88px; - } - - .nav-sub:before { - left: 88px; - } - } - - .nav-course-tools { - - .wrapper-nav-sub { - top: ($baseline*1.5); - width: 100px; - } - - .nav-sub:after { - left: 68px; - } - - .nav-sub:before { - left: 68px; - } - } - } -} - -// ==================== - -// STATE: is-signed in -.is-signedin { - - &.course .branding { - - &:before { - @extend .faded-vertical-divider; - content: ""; - display: block; - height: 50px; - position: absolute; - right: 1px; - top: -8px; - width: 1px; - } - - &:after { - @extend .faded-vertical-divider-light; - content: ""; - display: block; - height: 50px; - position: absolute; - right: 0px; - top: -12px; - width: 1px; - } - } -} - -// ==================== - -// STATE: not signed in -.not-signedin { - - .wrapper-left { - width: flex-grid(4, 12); - } - - .wrapper-right { - width: flex-grid(8, 12); - } - - // STATE: not signed in - unauthenticated nav - .nav-not-signedin { + .wrapper-r { float: right; - margin-top: ($baseline/4); + width: flex-grid(4,12); + text-align: right; + } - .nav-item { - @include font-size(16); + .branding, .info-course, .nav-course, .nav-account, .nav-pitch { + @include box-sizing(border-box); + display: inline-block; + vertical-align: middle; + } + + .nav-account { + width: 100%; + } + + // basic layout - nav items + nav { + + > ol > .nav-item { + @extend .t-action3; + display: inline-block; vertical-align: middle; - margin: 0 $baseline 0 0; + font-weight: 600; &:last-child { margin-right: 0; } + } - .action { - margin-top: -($baseline/4); - display: inline-block; - padding: ($baseline/4) ($baseline/2); + .nav-item a { + color: $gray-d1; + + &:hover { + color: $blue-s1; + } + } + } + + // basic layout - dropdowns + .nav-dd { + + .title { + @extend .t-action2; + @extend .btn-dd-nav-primary; + @include transition(all 0.25s ease-in-out 0); + + .label, .icon-caret-down { + + } + + .label { + + } + + .icon-caret-down { + opacity: 0.25; + } + + &:hover { + + .icon-caret-down { + opacity: 1.0; + } + } + + .nav-sub .nav-item { + + [class^="icon-"] { + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/4); + } + } + } + } + + // ==================== + + // specific elements - branding + .branding { + padding: ($baseline*0.75) 0; + + a { + display: block; + + img { + width: 100%; + display: block; + } + } + } + + // ==================== + + // specific elements - course name/info + .info-course { + margin-right: flex-gutter(); + border-right: 1px solid $gray-l4; + padding: ($baseline*0.75) flex-gutter() ($baseline*0.75) 0; + + .course-org, .course-number { + @extend .t-action4; + display: inline-block; + vertical-align: middle; + max-width: 45%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + opacity: 0.75; + } + + .course-org { + margin-right: ($baseline/4); + } + + .course-title { + @extend .t-action2; + display: block; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: 600; + } + + // entire link + .course-link { + @include transition(color 0.25s ease-in-out); + display: block; + color: $gray-d1; + + &:hover { + color: $blue-s1; + } + } + } + + // ==================== + + // specific elements - course nav + .nav-course { + padding: ($baseline*0.75) 0; + } + + // ==================== + + // specific elements - account-based nav + .nav-account { + position: relative; + padding: ($baseline*0.75) 0; + + .nav-sub { + text-align: left; + } + + .nav-account-help { + + .wrapper-nav-sub { + width: ($baseline*10); } } - // STATE: not signed in - specific items - .nav-not-signedin-help { + .nav-account-user { + .title { + max-width: ($baseline*6.5); + + > .label { + display: inline-block; + max-width: 85%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + } + + // ==================== + + // specific elements - pitch/how it works nav + .nav-pitch { + position: relative; + padding: ($baseline*0.75) 0; + + .nav-sub { + text-align: left; + } + } +} + +// ==================== + +// CASE: user signed in +.is-signedin { + + .wrapper-l { + width: flex-grid(9,12); + } + + .wrapper-r { + width: flex-grid(3,12); + } + + .branding { + width: 20%; + margin-right: 2%; + } + + .nav-account { + top: ($baseline/4); + } +} + +// ==================== + +// CASE: in course { +.is-signedin.course { + + .wrapper-header { + + .wrapper-l { + width: flex-grid(9,12); } - .nav-not-signedin-signup { - margin-right: ($baseline/2); + .wrapper-r { + width: flex-grid(3,12); + } + + .branding { + width: 20%; + margin-right: 2%; + } + + .info-course { + width: 25%; + margin-right: 2%; + } + + .nav-course { + width: 45%; + } + } +} + +// ==================== + +// CASE: user not signed in +.not-signedin { + + .wrapper-header { + + .wrapper-l { + width: flex-grid(2,12); + } + + .wrapper-r { + width: flex-grid(10,12); + } + + .branding { + width: 100%; + } + + .nav-pitch { + top: ($baseline/4); + + .nav-item { + margin-right: ($baseline/2); + + &:last-child { + margin-right: 0; + } + } .action-signup { @include blue-button; @@ -454,9 +316,6 @@ text-transform: uppercase; font-weight: 600; } - } - - .nav-not-signedin-signin { .action-signin { @include white-button; @@ -505,12 +364,13 @@ body.course.advanced .nav-course-settings-advanced, // course tools body.course.import .nav-course-tools .title, body.course.export .nav-course-tools .title, +body.course.checklists .nav-course-tools .title, body.course.import .nav-course-tools-import, body.course.export .nav-course-tools-export, +body.course.checklists .nav-course-tools-checklists, { - color: $blue; a { @@ -518,52 +378,3 @@ body.course.export .nav-course-tools-export, pointer-events: none; } } - -body.signup .nav-not-signedin-signin { - - a { - background-color: #d9e3ee; - color: #6d788b; - } -} - -body.signin .nav-not-signedin-signup { - - a { - background-color: #62aaf5; - color: #fff; - } -} - -// ==================== - -// STATE: js enabled -.js { - - .nav-dropdown { - - .nav-item .title { - outline: 0; - cursor: pointer; - - &:hover, &:active, &.is-selected { - color: $blue; - - .icon-expand { - color: $blue; - } - } - } - } - - .wrapper-nav-sub { - @include transition (opacity 1.0s ease-in-out 0s); - opacity: 0.0; - pointer-events: none; - - &.is-shown { - opacity: 1.0; - pointer-events: auto; - } - } -} diff --git a/cms/static/sass/elements/_navigation.scss b/cms/static/sass/elements/_navigation.scss index 516a19c7ad..4a6aacc529 100644 --- a/cms/static/sass/elements/_navigation.scss +++ b/cms/static/sass/elements/_navigation.scss @@ -18,20 +18,161 @@ nav { // ==================== -// primary - -// ==================== - -// right hand side - -// ==================== - // tabs // ==================== // dropdown +.nav-dd { -// ==================== + .title { -// + .label, .icon-caret-down { + display: inline-block; + vertical-align: middle; + } + + .ui-toggle-dd { + @include transition(rotate .25s ease-in-out .25s); + margin-left: ($baseline/10); + display: inline-block; + vertical-align: middle; + } + + // dropped down state + &.is-selected { + + .ui-toggle-dd { + @include transform(rotate(-180deg)); + @include transform-origin(50% 50%); + } + } + } + + .nav-item { + position: relative; + + &:hover { + + } + } + + .wrapper-nav-sub { + @include transition (opacity 1.0s ease-in-out 0s); + position: absolute; + top: ($baseline*2.5); + opacity: 0.0; + pointer-events: none; + width: ($baseline*8); + + + // dropped down state + &.is-shown { + opacity: 1.0; + pointer-events: auto; + } + } + + .nav-sub { + @include border-radius(2px); + @include box-sizing(border-box); + @include box-shadow(0 1px 1px $shadow-l1); + position: relative; + width: 100%; + border: 1px solid $gray-l3; + padding: ($baseline/2) ($baseline*0.75); + background: $white; + + &:after, &:before { + bottom: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + + // ui triangle/nub + &:after { + border-color: rgba(255, 255, 255, 0); + border-bottom-color: $white; + border-width: 10px; + } + + &:before { + border-color: rgba(178, 178, 178, 0); + border-bottom-color: $gray-l3; + border-width: 11px; + } + + .nav-item { + @extend .t-action3; + display: block; + margin: 0 0 ($baseline/4) 0; + border-bottom: 1px solid $gray-l5; + padding: 0 0($baseline/4) 0; + font-weight: 500; + + &:last-child { + margin-bottom: 0; + border-bottom: none; + padding-bottom: 0; + } + + a { + display: block; + + &:hover, &:active { + color: $blue-s1; + } + } + } + } + + // CASE: left-hand side arrow/dd + &.ui-left { + + .wrapper-nav-sub { + left: 0; + } + + .nav-sub { + text-align: left; + + // ui triangle/nub + &:after { + left: $baseline; + margin-left: -10px; + } + + &:before { + left: $baseline; + margin-left: -11px; + } + } + } + + // CASE: right-hand side arrow/dd + &.ui-right { + + .wrapper-nav-sub { + left: none; + right: 0; + } + + .nav-sub { + + // ui triangle/nub + &:after { + right: $baseline; + margin-right: -10px; + } + + &:before { + right: $baseline; + margin-right: -11px; + } + } + } +} diff --git a/cms/static/sass/elements/_tender-widget.scss b/cms/static/sass/elements/_tender-widget.scss index b4113732f0..b00d38f1ce 100644 --- a/cms/static/sass/elements/_tender-widget.scss +++ b/cms/static/sass/elements/_tender-widget.scss @@ -11,7 +11,7 @@ @include box-shadow(0 2px 3px $shadow); height: ($baseline*35) !important; background: $white !important; - border: 1px solid $gray; + border: 2px solid $blue; } #tender_window { @@ -23,11 +23,12 @@ } #tender_closer { - color: $blue-l2 !important; + color: $white-t2 !important; text-transform: uppercase; + top: 16px !important; &:hover { - color: $blue-l4 !important; + color: $white !important; } } @@ -42,15 +43,15 @@ font-family: 'Open Sans', sans-serif; } -.widget-layout .search, -.widget-layout .tabs, -.widget-layout .footer, +.widget-layout .search, +.widget-layout .tabs, +.widget-layout .footer, .widget-layout .header h1 a { display: none; } .widget-layout .header { - background: rgb(85, 151, 221); + background: rgb(0, 159, 230); padding: 10px 20px; } @@ -264,4 +265,4 @@ .widget-layout .form-actions .btn-post_topic:hover, .widget-layout .form-actions .btn-post_topic:active { background-color: #16ca57; color: #fff; -} \ No newline at end of file +} diff --git a/cms/static/sass/views/_index.scss b/cms/static/sass/views/_index.scss index cb6df23c3c..fdd2eec6bf 100644 --- a/cms/static/sass/views/_index.scss +++ b/cms/static/sass/views/_index.scss @@ -72,14 +72,7 @@ body.index { } .logo { - @extend .text-hide; - position: relative; - top: 3px; - display: inline-block; - vertical-align: baseline; - width: 282px; - height: 57px; - background: transparent url('../img/logo-edx-studio-white.png') 0 0 no-repeat; + font-weight: 600; } .tagline { diff --git a/cms/static/sass/views/_settings.scss b/cms/static/sass/views/_settings.scss index cbb1034626..d815663d76 100644 --- a/cms/static/sass/views/_settings.scss +++ b/cms/static/sass/views/_settings.scss @@ -316,6 +316,12 @@ body.course.settings { .link-courseURL { @extend .t-copy-lead1; + @include box-sizing(border-box); + display: block; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; &:hover { diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index abbc5bb1b4..56788ea399 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -40,7 +40,7 @@

- Course Content + Content > Files & Uploads

diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index b13a024fde..a1ab3e694c 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -44,7 +44,7 @@

- Course Content + Content > Course Updates

diff --git a/cms/templates/edit-tabs.html b/cms/templates/edit-tabs.html index 77eb1cb9b8..7fa1510065 100644 --- a/cms/templates/edit-tabs.html +++ b/cms/templates/edit-tabs.html @@ -19,7 +19,7 @@

- Course Content + Content > Static Pages

diff --git a/cms/templates/overview.html b/cms/templates/overview.html index a504d50019..8a52542a04 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -121,7 +121,7 @@

- Course Content + Content > Course Outline

@@ -165,9 +165,9 @@ This section has not been released. Schedule %else: - Will Release: + Will Release: ${date_utils.get_default_time_display(section.lms.start)} - Edit %endif
diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 3c8a598b5e..a699b976cf 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -1,64 +1,85 @@ <%! from django.core.urlresolvers import reverse %> -
- + ${course.number} ${get_course_about_section(course, 'title')} Cover Image

${get_course_about_section(course, 'short_description')}

diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 7f0d596f4b..f731f0d989 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -10,9 +10,9 @@
% if self.stanford_theme_enabled(): diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index bc49cda427..f5abe3476d 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -104,7 +104,7 @@ function goto( mode)

Instructor Dashboard

-

[ Grades | +

-