diff --git a/.gitignore b/.gitignore index 8fb170c30f..d01baf055a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ :2e# .AppleDouble database.sqlite +private-requirements.txt courseware/static/js/mathjax/* flushdb.sh build @@ -31,3 +32,4 @@ cover_html/ chromedriver.log /nbproject ghostdriver.log +node_modules diff --git a/.pylintrc b/.pylintrc index 9ea1e62ad4..792079ce03 100644 --- a/.pylintrc +++ b/.pylintrc @@ -34,15 +34,23 @@ load-plugins= # multiple time (only on the command line, not in the configuration file where # it should appear only once). disable= -# W0141: Used builtin function 'map' +# Never going to use these +# C0301: Line too long # W0142: Used * or ** magic +# W0141: Used builtin function 'map' + +# Might use these when the code is in better shape +# C0302: Too many lines in module # R0201: Method could be a function # R0901: Too many ancestors # 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 - 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] @@ -91,7 +99,18 @@ 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, + status_code [BASIC] diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000000..93a8706d3e --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +mitx diff --git a/apt-packages.txt b/apt-packages.txt index 0560dfcbc2..2635388757 100644 --- a/apt-packages.txt +++ b/apt-packages.txt @@ -22,5 +22,4 @@ libreadline6 libreadline6-dev mongodb nodejs -npm coffeescript diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 281e3f46b2..71b5e97bc2 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -1,6 +1,3 @@ -import logging -import sys - from django.contrib.auth.models import User, Group from django.core.exceptions import PermissionDenied @@ -131,7 +128,7 @@ def remove_user_from_course_group(caller, user, location, role): raise PermissionDenied # see if the user is actually in that role, if not then we don't have to do anything - if is_user_in_course_group_role(user, location, role) == True: + if is_user_in_course_group_role(user, location, role): groupname = get_course_groupname_for_role(location, role) group = Group.objects.get(name=groupname) diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 589db4ac56..ada3873992 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -97,8 +97,7 @@ def update_course_updates(location, update, passed_id=None): if (len(new_html_parsed) == 1): content = new_html_parsed[0].tail else: - content = "\n".join([html.tostring(ele) - for ele in new_html_parsed[1:]]) + content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]]) return {"id": passed_id, "date": update['date'], diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature index af97709ad0..ca5b62e596 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.feature +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -1,6 +1,6 @@ Feature: Advanced (manual) course policy In order to specify course policy settings for which no custom user interface exists - I want to be able to manually enter JSON key/value pairs + I want to be able to manually enter JSON key /value pairs Scenario: A course author sees default advanced settings Given I have opened a new course in Studio @@ -11,6 +11,7 @@ Feature: Advanced (manual) course policy Given I am on the Advanced Course Settings page in Studio Then the settings are alphabetized + @skip-phantom Scenario: Test cancel editing key value Given I am on the Advanced Course Settings page in Studio When I edit the value of a policy key @@ -19,14 +20,15 @@ Feature: Advanced (manual) course policy And I reload the page Then the policy key value is unchanged + @skip-phantom 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 + @skip-phantom Scenario: Test how multi-line input appears Given I am on the Advanced Course Settings page in Studio When I create a JSON object as a value @@ -34,6 +36,7 @@ Feature: Advanced (manual) course policy And I reload the page Then it is displayed as formatted + @skip-phantom Scenario: Test automatic quoting of non-JSON values Given I am on the Advanced Course Settings page in Studio When I create a non-JSON value not in quotes diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py index 7e86e94a31..ea5b24b21f 100644 --- a/cms/djangoapps/contentstore/features/advanced-settings.py +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -1,11 +1,9 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * -import time -from terrain.steps import reload_the_page -from selenium.common.exceptions import WebDriverException -from selenium.webdriver.support import expected_conditions as EC - -from nose.tools import assert_true, assert_false, assert_equal +from nose.tools import assert_false, assert_equal """ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html @@ -17,14 +15,15 @@ VALUE_CSS = 'textarea.json' DISPLAY_NAME_KEY = "display_name" DISPLAY_NAME_VALUE = '"Robot Super Course"' + ############### ACTIONS #################### @step('I select the Advanced Settings$') def i_select_advanced_settings(step): expand_icon_css = 'li.nav-course-settings i.icon-expand' if world.browser.is_element_present_by_css(expand_icon_css): - css_click(expand_icon_css) + world.css_click(expand_icon_css) link_css = 'li.nav-course-settings-advanced a' - css_click(link_css) + world.css_click(link_css) @step('I am on the Advanced Course Settings page in Studio$') @@ -35,24 +34,8 @@ def i_am_on_advanced_course_settings(step): @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(step, name): - def is_visible(driver): - return EC.visibility_of_element_located((By.CSS_SELECTOR, css,)) - - # def is_invisible(driver): - # return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,)) - css = 'a.%s-button' % name.lower() - wait_for(is_visible) - time.sleep(float(1)) - css_click_at(css) - -# is_invisible is not returning a boolean, not working -# try: -# css_click_at(css) -# wait_for(is_invisible) -# except WebDriverException, e: -# css_click_at(css) -# wait_for(is_invisible) + world.css_click(css) @step(u'I edit the value of a policy key$') @@ -61,10 +44,15 @@ def edit_the_value_of_a_policy_key(step): It is hard to figure out how to get into the CodeMirror area, so cheat and do it from the policy key field :) """ - e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] + e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] 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_and_save(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"}') @@ -85,7 +73,7 @@ def i_see_default_advanced_settings(step): @step('the settings are alphabetized$') def they_are_alphabetized(step): - key_elements = css_find(KEY_CSS) + key_elements = world.css_find(KEY_CSS) all_keys = [] for key in key_elements: all_keys.append(key.value) @@ -99,7 +87,7 @@ def it_is_formatted(step): @step('it is displayed as a string') -def it_is_formatted(step): +def it_is_displayed_as_string(step): assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"']) @@ -110,7 +98,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 ############### @@ -118,13 +106,13 @@ def assert_policy_entries(expected_keys, expected_values): for counter in range(len(expected_keys)): index = get_index_of(expected_keys[counter]) assert_false(index == -1, "Could not find key: " + expected_keys[counter]) - assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect") + assert_equal(expected_values[counter], world.css_find(VALUE_CSS)[index].value, "value is incorrect") def get_index_of(expected_key): - for counter in range(len(css_find(KEY_CSS))): + for counter in range(len(world.css_find(KEY_CSS))): # Sometimes get stale reference if I hold on to the array of elements - key = css_find(KEY_CSS)[counter].value + key = world.css_find(KEY_CSS)[counter].value if key == expected_key: return counter @@ -133,14 +121,14 @@ def get_index_of(expected_key): def get_display_name_value(): index = get_index_of(DISPLAY_NAME_KEY) - return css_find(VALUE_CSS)[index].value + return world.css_find(VALUE_CSS)[index].value def change_display_name_value(step, new_value): - e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] + e = world.css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)] display_name = get_display_name_value() for count in range(len(display_name)): e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE) # Must delete "" before typing the JSON value e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value) - press_the_notification_button(step, "Save") \ No newline at end of file + press_the_notification_button(step, "Save") diff --git a/cms/djangoapps/contentstore/features/checklists.feature b/cms/djangoapps/contentstore/features/checklists.feature index bccb80b8d7..ddf1adf263 100644 --- a/cms/djangoapps/contentstore/features/checklists.feature +++ b/cms/djangoapps/contentstore/features/checklists.feature @@ -10,6 +10,8 @@ Feature: Course checklists Then I can check and uncheck tasks in a checklist And They are correctly selected after I reload the page + @skip-phantom + @skip-firefox Scenario: A task can link to a location within Studio Given I have opened Checklists When I select a link to the course outline @@ -17,8 +19,9 @@ Feature: Course checklists And I press the browser back button Then I am brought back to the course outline in the correct state + @skip-phantom + @skip-firefox Scenario: A task can link to a location outside Studio Given I have opened Checklists When I select a link to help page Then I am brought to the help page in a new window - diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py index 9ef66c8096..d433dbbf0d 100644 --- a/cms/djangoapps/contentstore/features/checklists.py +++ b/cms/djangoapps/contentstore/features/checklists.py @@ -1,15 +1,20 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step -from common import * +from nose.tools import assert_true, assert_equal from terrain.steps import reload_the_page +from selenium.common.exceptions import StaleElementReferenceException + ############### ACTIONS #################### @step('I select Checklists from the Tools menu$') def i_select_checklists(step): expand_icon_css = 'li.nav-course-tools i.icon-expand' if world.browser.is_element_present_by_css(expand_icon_css): - css_click(expand_icon_css) + world.css_click(expand_icon_css) link_css = 'li.nav-course-tools-checklists a' - css_click(link_css) + world.css_click(link_css) @step('I have opened Checklists$') @@ -20,7 +25,7 @@ def i_have_opened_checklists(step): @step('I see the four default edX checklists$') def i_see_default_checklists(step): - checklists = css_find('.checklist-title') + checklists = world.css_find('.checklist-title') assert_equal(4, len(checklists)) assert_true(checklists[0].text.endswith('Getting Started With Studio')) assert_true(checklists[1].text.endswith('Draft a Rough Course Outline')) @@ -58,7 +63,7 @@ def i_select_a_link_to_the_course_outline(step): @step('I am brought to the course outline page$') def i_am_brought_to_course_outline(step): - assert_equal('Course Outline', css_find('.outline .title-1')[0].text) + assert_equal('Course Outline', world.css_find('.outline .title-1')[0].text) assert_equal(1, len(world.browser.windows)) @@ -84,36 +89,35 @@ def i_am_brought_to_help_page_in_new_window(step): assert_equal('http://help.edge.edx.org/', world.browser.url) - - ############### HELPER METHODS #################### def verifyChecklist2Status(completed, total, percentage): def verify_count(driver): try: - statusCount = css_find('#course-checklist1 .status-count').first + statusCount = world.css_find('#course-checklist1 .status-count').first return statusCount.text == str(completed) except StaleElementReferenceException: return False - wait_for(verify_count) - assert_equal(str(total), css_find('#course-checklist1 .status-amount').first.text) + world.wait_for(verify_count) + assert_equal(str(total), world.css_find('#course-checklist1 .status-amount').first.text) # Would like to check the CSS width, but not sure how to do that. - assert_equal(str(percentage), css_find('#course-checklist1 .viz-checklist-status-value .int').first.text) + assert_equal(str(percentage), world.css_find('#course-checklist1 .viz-checklist-status-value .int').first.text) def toggleTask(checklist, task): - css_click('#course-checklist' + str(checklist) +'-task' + str(task)) + world.css_click('#course-checklist' + str(checklist) + '-task' + str(task)) +# TODO: figure out a way to do this in phantom and firefox +# For now we will mark the scenerios that use this method as skipped def clickActionLink(checklist, task, actionText): # toggle checklist item to make sure that the link button is showing toggleTask(checklist, task) - action_link = css_find('#course-checklist' + str(checklist) + ' a')[task] + action_link = world.css_find('#course-checklist' + str(checklist) + ' a')[task] # text will be empty initially, wait for it to populate def verify_action_link_text(driver): return action_link.text == actionText - wait_for(verify_action_link_text) + world.wait_for(verify_action_link_text) action_link.click() - diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 820b60123b..afb38c3f9e 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -1,28 +1,30 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step -from lettuce.django import django_url from nose.tools import assert_true from nose.tools import assert_equal -from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import WebDriverException, StaleElementReferenceException -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.by import By from xmodule.modulestore.django import _MODULESTORES, modulestore from xmodule.templates import update_templates from auth.authz import get_user_by_email +from selenium.webdriver.common.keys import Keys +import time + from logging import getLogger logger = getLogger(__name__) ########### STEP HELPERS ############## + @step('I (?:visit|access|open) the Studio homepage$') def i_visit_the_studio_homepage(step): # To make this go to port 8001, put # LETTUCE_SERVER_PORT = 8001 # in your settings.py file. - world.browser.visit(django_url('/')) + world.visit('/') signin_css = 'a.action-signin' - assert world.browser.is_element_present_by_css(signin_css, 10) + assert world.is_css_present(signin_css) @step('I am logged into Studio$') @@ -43,12 +45,12 @@ def i_press_the_category_delete_icon(step, category): css = 'a.delete-button.delete-subsection-button span.delete-icon' else: assert False, 'Invalid category: %s' % category - css_click(css) + world.css_click(css) @step('I have opened a new course in Studio$') def i_have_opened_a_new_course(step): - clear_courses() + world.clear_courses() log_into_studio() create_a_course() @@ -74,80 +76,13 @@ def create_studio_user( user_profile = world.UserProfileFactory(user=studio_user) -def flush_xmodule_store(): - # Flush and initialize the module store - # It needs the templates because it creates new records - # by cloning from the template. - # Note that if your test module gets in some weird state - # (though it shouldn't), do this manually - # from the bash shell to drop it: - # $ mongo test_xmodule --eval "db.dropDatabase()" - _MODULESTORES = {} - modulestore().collection.drop() - update_templates() - - -def assert_css_with_text(css, text): - assert_true(world.browser.is_element_present_by_css(css, 5)) - assert_equal(world.browser.find_by_css(css).text, text) - - -def css_click(css): - ''' - First try to use the regular click method, - but if clicking in the middle of an element - doesn't work it might be that it thinks some other - element is on top of it there so click in the upper left - ''' - try: - css_find(css).first.click() - except WebDriverException, e: - css_click_at(css) - - -def css_click_at(css, x=10, y=10): - ''' - A method to click at x,y coordinates of the element - rather than in the center of the element - ''' - e = css_find(css).first - e.action_chains.move_to_element_with_offset(e._element, x, y) - e.action_chains.click() - e.action_chains.perform() - - -def css_fill(css, value): - world.browser.find_by_css(css).first.fill(value) - - -def css_find(css): - def is_visible(driver): - return EC.visibility_of_element_located((By.CSS_SELECTOR,css,)) - - world.browser.is_element_present_by_css(css, 5) - wait_for(is_visible) - return world.browser.find_by_css(css) - - -def wait_for(func): - WebDriverWait(world.browser.driver, 5).until(func) - - -def id_find(id): - return world.browser.find_by_id(id) - - -def clear_courses(): - flush_xmodule_store() - - def fill_in_course_info( name='Robot Super Course', org='MITx', num='101'): - css_fill('.new-course-name', name) - css_fill('.new-course-org', org) - css_fill('.new-course-number', num) + world.css_fill('.new-course-name', name) + world.css_fill('.new-course-org', org) + world.css_fill('.new-course-number', num) def log_into_studio( @@ -155,21 +90,22 @@ def log_into_studio( email='robot+studio@edx.org', password='test', is_staff=False): - create_studio_user(uname=uname, email=email, is_staff=is_staff) - world.browser.cookies.delete() - world.browser.visit(django_url('/')) - signin_css = 'a.action-signin' - world.browser.is_element_present_by_css(signin_css, 10) - # click the signin button - css_click(signin_css) + create_studio_user(uname=uname, email=email, is_staff=is_staff) + + world.browser.cookies.delete() + world.visit('/') + + signin_css = 'a.action-signin' + world.is_css_present(signin_css) + world.css_click(signin_css) login_form = world.browser.find_by_css('form#login_form') login_form.find_by_name('email').fill(email) login_form.find_by_name('password').fill(password) login_form.find_by_name('submit').click() - assert_true(world.browser.is_element_present_by_css('.new-course-button', 5)) + assert_true(world.is_css_present('.new-course-button')) def create_a_course(): @@ -184,26 +120,37 @@ def create_a_course(): world.browser.reload() course_link_css = 'span.class-name' - css_click(course_link_css) + world.css_click(course_link_css) course_title_css = 'span.course-title' - assert_true(world.browser.is_element_present_by_css(course_title_css, 5)) + assert_true(world.is_css_present(course_title_css)) def add_section(name='My Section'): link_css = 'a.new-courseware-section-button' - css_click(link_css) + world.css_click(link_css) name_css = 'input.new-section-name' save_css = 'input.new-section-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) span_css = 'span.section-name-span' - assert_true(world.browser.is_element_present_by_css(span_css, 5)) + assert_true(world.is_css_present(span_css)) def add_subsection(name='Subsection One'): css = 'a.new-subsection-item' - css_click(css) + world.css_click(css) name_css = 'input.new-subsection-name-input' save_css = 'input.new-subsection-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) + + +def set_date_and_time(date_css, desired_date, time_css, desired_time): + world.css_fill(date_css, desired_date) + # hit TAB to get to the time field + e = world.css_find(date_css).first + e._element.send_keys(Keys.TAB) + world.css_fill(time_css, desired_time) + e = world.css_find(time_css).first + e._element.send_keys(Keys.TAB) + time.sleep(float(1)) diff --git a/cms/djangoapps/contentstore/features/course-settings.feature b/cms/djangoapps/contentstore/features/course-settings.feature new file mode 100644 index 0000000000..fc9641cb46 --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-settings.feature @@ -0,0 +1,28 @@ +Feature: Course Settings + As a course author, I want to be able to configure my course settings. + + @skip-phantom + Scenario: User can set course dates + Given I have opened a new course in Studio + When I select Schedule and Details + And I set course dates + Then I see the set dates on refresh + + @skip-phantom + Scenario: User can clear previously set course dates (except start date) + Given I have set course dates + And I clear all the dates except start + Then I see cleared dates on refresh + + @skip-phantom + Scenario: User cannot clear the course start date + Given I have set course dates + And I clear the course start date + Then I receive a warning about course start date + And The previously set start date is shown on refresh + + Scenario: User can correct the course start date warning + Given I have tried to clear the course start + And I have entered a new course start date + Then The warning about course start date goes away + And My new course start date is shown on refresh diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py new file mode 100644 index 0000000000..d69266b7de --- /dev/null +++ b/cms/djangoapps/contentstore/features/course-settings.py @@ -0,0 +1,165 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + +from lettuce import world, step +from terrain.steps import reload_the_page +from selenium.webdriver.common.keys import Keys +import time + +from nose.tools import assert_true, assert_false, assert_equal + +COURSE_START_DATE_CSS = "#course-start-date" +COURSE_END_DATE_CSS = "#course-end-date" +ENROLLMENT_START_DATE_CSS = "#course-enrollment-start-date" +ENROLLMENT_END_DATE_CSS = "#course-enrollment-end-date" + +COURSE_START_TIME_CSS = "#course-start-time" +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 = "15:30" +DEFAULT_TIME = "00:00" + + +############### ACTIONS #################### +@step('I select Schedule and Details$') +def test_i_select_schedule_and_details(step): + expand_icon_css = 'li.nav-course-settings i.icon-expand' + if world.browser.is_element_present_by_css(expand_icon_css): + world.css_click(expand_icon_css) + link_css = 'li.nav-course-settings-schedule a' + world.css_click(link_css) + + +@step('I have set course dates$') +def test_i_have_set_course_dates(step): + step.given('I have opened a new course in Studio') + step.given('I select Schedule and Details') + step.given('And I set course dates') + + +@step('And I set course dates$') +def test_and_i_set_course_dates(step): + set_date_or_time(COURSE_START_DATE_CSS, '12/20/2013') + set_date_or_time(COURSE_END_DATE_CSS, '12/26/2013') + set_date_or_time(ENROLLMENT_START_DATE_CSS, '12/1/2013') + set_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013') + + set_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + set_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME) + + pause() + + +@step('Then I see the set dates on refresh$') +def test_then_i_see_the_set_dates_on_refresh(step): + reload_the_page(step) + verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013') + verify_date_or_time(COURSE_END_DATE_CSS, '12/26/2013') + verify_date_or_time(ENROLLMENT_START_DATE_CSS, '12/01/2013') + verify_date_or_time(ENROLLMENT_END_DATE_CSS, '12/10/2013') + + verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + # Unset times get set to 12 AM once the corresponding date has been set. + verify_date_or_time(COURSE_END_TIME_CSS, DEFAULT_TIME) + verify_date_or_time(ENROLLMENT_START_TIME_CSS, DEFAULT_TIME) + verify_date_or_time(ENROLLMENT_END_TIME_CSS, DUMMY_TIME) + + +@step('And I clear all the dates except start$') +def test_and_i_clear_all_the_dates_except_start(step): + set_date_or_time(COURSE_END_DATE_CSS, '') + set_date_or_time(ENROLLMENT_START_DATE_CSS, '') + set_date_or_time(ENROLLMENT_END_DATE_CSS, '') + + pause() + + +@step('Then I see cleared dates on refresh$') +def test_then_i_see_cleared_dates_on_refresh(step): + reload_the_page(step) + verify_date_or_time(COURSE_END_DATE_CSS, '') + verify_date_or_time(ENROLLMENT_START_DATE_CSS, '') + verify_date_or_time(ENROLLMENT_END_DATE_CSS, '') + + verify_date_or_time(COURSE_END_TIME_CSS, '') + verify_date_or_time(ENROLLMENT_START_TIME_CSS, '') + verify_date_or_time(ENROLLMENT_END_TIME_CSS, '') + + # Verify course start date (required) and time still there + verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013') + verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + + +@step('I clear the course start date$') +def test_i_clear_the_course_start_date(step): + set_date_or_time(COURSE_START_DATE_CSS, '') + + +@step('I receive a warning about course start date$') +def test_i_receive_a_warning_about_course_start_date(step): + assert_true(world.css_has_text('.message-error', 'The course must have an assigned start date.')) + assert_true('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class')) + assert_true('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class')) + + +@step('The previously set start date is shown on refresh$') +def test_the_previously_set_start_date_is_shown_on_refresh(step): + reload_the_page(step) + verify_date_or_time(COURSE_START_DATE_CSS, '12/20/2013') + verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + + +@step('Given I have tried to clear the course start$') +def test_i_have_tried_to_clear_the_course_start(step): + step.given("I have set course dates") + step.given("I clear the course start date") + step.given("I receive a warning about course start date") + + +@step('I have entered a new course start date$') +def test_i_have_entered_a_new_course_start_date(step): + set_date_or_time(COURSE_START_DATE_CSS, '12/22/2013') + pause() + + +@step('The warning about course start date goes away$') +def test_the_warning_about_course_start_date_goes_away(step): + assert_equal(0, len(world.css_find('.message-error'))) + assert_false('error' in world.css_find(COURSE_START_DATE_CSS).first._element.get_attribute('class')) + assert_false('error' in world.css_find(COURSE_START_TIME_CSS).first._element.get_attribute('class')) + + +@step('My new course start date is shown on refresh$') +def test_my_new_course_start_date_is_shown_on_refresh(step): + reload_the_page(step) + verify_date_or_time(COURSE_START_DATE_CSS, '12/22/2013') + # Time should have stayed from before attempt to clear date. + verify_date_or_time(COURSE_START_TIME_CSS, DUMMY_TIME) + + +############### HELPER METHODS #################### +def set_date_or_time(css, date_or_time): + """ + Sets date or time field. + """ + world.css_fill(css, date_or_time) + e = world.css_find(css).first + # hit Enter to apply the changes + e._element.send_keys(Keys.ENTER) + + +def verify_date_or_time(css, date_or_time): + """ + Verifies date or time field. + """ + assert_equal(date_or_time, world.css_find(css).first.value) + + +def pause(): + """ + Must sleep briefly to allow last time save to finish, + else refresh of browser will fail. + """ + time.sleep(float(1)) diff --git a/cms/djangoapps/contentstore/features/courses.feature b/cms/djangoapps/contentstore/features/courses.feature index 39d39b50aa..455313b0e2 100644 --- a/cms/djangoapps/contentstore/features/courses.feature +++ b/cms/djangoapps/contentstore/features/courses.feature @@ -10,4 +10,4 @@ Feature: Create Course And I fill in the new course information And I press the "Save" button Then the Courseware page has loaded in Studio - And I see a link for adding a new section \ No newline at end of file + And I see a link for adding a new section diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index e394165f08..5da7720945 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * @@ -6,12 +9,12 @@ from common import * @step('There are no courses$') def no_courses(step): - clear_courses() + world.clear_courses() @step('I click the New Course button$') def i_click_new_course(step): - css_click('.new-course-button') + world.css_click('.new-course-button') @step('I fill in the new course information$') @@ -27,7 +30,7 @@ def i_create_a_course(step): @step('I click the course link in My Courses$') def i_click_the_course_link_in_my_courses(step): course_css = 'span.class-name' - css_click(course_css) + world.css_click(course_css) ############ ASSERTIONS ################### @@ -35,28 +38,28 @@ def i_click_the_course_link_in_my_courses(step): @step('the Courseware page has loaded in Studio$') def courseware_page_has_loaded_in_studio(step): course_title_css = 'span.course-title' - assert world.browser.is_element_present_by_css(course_title_css) + assert world.is_css_present(course_title_css) @step('I see the course listed in My Courses$') def i_see_the_course_in_my_courses(step): course_css = 'span.class-name' - assert_css_with_text(course_css, 'Robot Super Course') + assert world.css_has_text(course_css, 'Robot Super Course') @step('the course is loaded$') def course_is_loaded(step): class_css = 'a.class-name' - assert_css_with_text(class_css, 'Robot Super Course') + assert world.css_has_text(course_css, 'Robot Super Cousre') @step('I am on the "([^"]*)" tab$') def i_am_on_tab(step, tab_name): header_css = 'div.inner-wrapper h1' - assert_css_with_text(header_css, tab_name) + assert world.css_has_text(header_css, tab_name) @step('I see a link for adding a new section$') def i_see_new_section_link(step): link_css = 'a.new-courseware-section-button' - assert_css_with_text(link_css, '+ New Section') + assert world.css_has_text(link_css, '+ New Section') diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index 08d38367bc..24cbeb3db9 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -3,6 +3,7 @@ Feature: Create Section As a course author I want to create and edit sections + @skip-phantom Scenario: Add a new section to a course Given I have opened a new course in Studio When I click the New Section link diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index b5ddb48a09..59c5a37b33 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -1,8 +1,9 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * from nose.tools import assert_equal -from selenium.webdriver.common.keys import Keys -import time ############### ACTIONS #################### @@ -10,7 +11,7 @@ import time @step('I click the new section link$') def i_click_new_section_link(step): link_css = 'a.new-courseware-section-button' - css_click(link_css) + world.css_click(link_css) @step('I enter the section name and click save$') @@ -31,21 +32,13 @@ def i_have_added_new_section(step): @step('I click the Edit link for the release date$') def i_click_the_edit_link_for_the_release_date(step): button_css = 'div.section-published-date a.edit-button' - css_click(button_css) + world.css_click(button_css) @step('I save a new section release date$') def i_save_a_new_section_release_date(step): - date_css = 'input.start-date.date.hasDatepicker' - time_css = 'input.start-time.time.ui-timepicker-input' - css_fill(date_css, '12/25/2013') - # hit TAB to get to the time field - e = css_find(date_css).first - e._element.send_keys(Keys.TAB) - css_fill(time_css, '12:00am') - e = css_find(time_css).first - e._element.send_keys(Keys.TAB) - time.sleep(float(1)) + set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013', + 'input.start-time.time.ui-timepicker-input', '00:00') world.browser.click_link_by_text('Save') @@ -64,13 +57,13 @@ def i_see_my_section_name_with_quote_on_the_courseware_page(step): @step('I click to edit the section name$') def i_click_to_edit_section_name(step): - css_click('span.section-name-span') + world.css_click('span.section-name-span') @step('I see the complete section name with a quote in the editor$') def i_see_complete_section_name_with_quote_in_editor(step): css = '.edit-section-name' - assert world.browser.is_element_present_by_css(css, 5) + assert world.is_css_present(css) assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"') @@ -85,7 +78,7 @@ def i_see_a_release_date_for_my_section(step): import re css = 'span.published-status' - assert world.browser.is_element_present_by_css(css) + assert world.is_css_present(css) status_text = world.browser.find_by_css(css).text # e.g. 11/06/2012 at 16:25 @@ -99,20 +92,20 @@ def i_see_a_release_date_for_my_section(step): @step('I see a link to create a new subsection$') def i_see_a_link_to_create_a_new_subsection(step): css = 'a.new-subsection-item' - assert world.browser.is_element_present_by_css(css) + assert world.is_css_present(css) @step('the section release date picker is not visible$') def the_section_release_date_picker_not_visible(step): css = 'div.edit-subsection-publish-settings' - assert False, world.browser.find_by_css(css).visible + assert not world.css_visible(css) @step('the section release date is updated$') def the_section_release_date_is_updated(step): css = 'span.published-status' - status_text = world.browser.find_by_css(css).text - assert_equal(status_text,'Will Release: 12/25/2013 at 12:00am') + status_text = world.css_text(css) + assert_equal(status_text, 'Will Release: 12/25/2013 at 00:00 UTC') ############ HELPER METHODS ################### @@ -120,10 +113,10 @@ def the_section_release_date_is_updated(step): def save_section_name(name): name_css = '.new-section-name' save_css = '.new-section-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) def see_my_section_on_the_courseware_page(name): section_css = 'span.section-name-span' - assert_css_with_text(section_css, name) + assert world.css_has_text(section_css, name) diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index e8d0dd8229..6ca358183b 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * @@ -17,9 +20,10 @@ def i_press_the_button_on_the_registration_form(step): submit_css = 'form#register_form button#submit' # Workaround for click not working on ubuntu # for some unknown reason. - e = css_find(submit_css) + e = world.css_find(submit_css) e.type(' ') + @step('I should see be on the studio home page$') def i_should_see_be_on_the_studio_home_page(step): assert world.browser.find_by_css('div.inner-wrapper') diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature index 52c10e41a8..a0e0a48f9e 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.feature @@ -1,32 +1,33 @@ Feature: Overview Toggle Section In order to quickly view the details of a course's section or to scan the inventory of sections - As a course author - I want to toggle the visibility of each section's subsection details in the overview listing + As a course author + I want to toggle the visibility of each section's subsection details in the overview listing Scenario: The default layout for the overview page is to show sections in expanded view Given I have a course with multiple sections - When I navigate to the course overview page - Then I see the "Collapse All Sections" link - And all sections are expanded + When I navigate to the course overview page + Then I see the "Collapse All Sections" link + And all sections are expanded - Scenario: Expand/collapse for a course with no sections + Scenario: Expand /collapse for a course with no sections Given I have a course with no sections - When I navigate to the course overview page - Then I do not see the "Collapse All Sections" link + When I navigate to the course overview page + Then I do not see the "Collapse All Sections" link + @skip-phantom Scenario: Collapse link appears after creating first section of a course Given I have a course with no sections - When I navigate to the course overview page - And I add a section - Then I see the "Collapse All Sections" link - And all sections are expanded + When I navigate to the course overview page + And I add a section + Then I see the "Collapse All Sections" link + And all sections are expanded @skip-phantom Scenario: Collapse link is not removed after last section of a course is deleted Given I have a course with 1 section - And I navigate to the course overview page - When I press the "section" delete icon - And I confirm the alert + And I navigate to the course overview page + When I press the "section" delete icon + And I confirm the alert Then I see the "Collapse All Sections" link Scenario: Collapsing all sections when all sections are expanded @@ -57,4 +58,4 @@ Feature: Overview Toggle Section When I expand the first section And I click the "Expand All Sections" link Then I see the "Collapse All Sections" link - And all sections are expanded \ No newline at end of file + And all sections are expanded diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 060d592cfd..7f717b731c 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * from nose.tools import assert_true, assert_false, assert_equal @@ -8,13 +11,13 @@ logger = getLogger(__name__) @step(u'I have a course with no sections$') def have_a_course(step): - clear_courses() + world.clear_courses() course = world.CourseFactory.create() @step(u'I have a course with 1 section$') def have_a_course_with_1_section(step): - clear_courses() + world.clear_courses() course = world.CourseFactory.create() section = world.ItemFactory.create(parent_location=course.location) subsection1 = world.ItemFactory.create( @@ -25,7 +28,7 @@ def have_a_course_with_1_section(step): @step(u'I have a course with multiple sections$') def have_a_course_with_two_sections(step): - clear_courses() + world.clear_courses() course = world.CourseFactory.create() section = world.ItemFactory.create(parent_location=course.location) subsection1 = world.ItemFactory.create( @@ -49,7 +52,7 @@ def have_a_course_with_two_sections(step): def navigate_to_the_course_overview_page(step): log_into_studio(is_staff=True) course_locator = '.class-name' - css_click(course_locator) + world.css_click(course_locator) @step(u'I navigate to the courseware page of a course with multiple sections') @@ -66,44 +69,44 @@ def i_add_a_section(step): @step(u'I click the "([^"]*)" link$') def i_click_the_text_span(step, text): span_locator = '.toggle-button-sections span' - assert_true(world.browser.is_element_present_by_css(span_locator, 5)) + assert_true(world.browser.is_element_present_by_css(span_locator)) # first make sure that the expand/collapse text is the one you expected assert_equal(world.browser.find_by_css(span_locator).value, text) - css_click(span_locator) + world.css_click(span_locator) @step(u'I collapse the first section$') def i_collapse_a_section(step): collapse_locator = 'section.courseware-section a.collapse' - css_click(collapse_locator) + world.css_click(collapse_locator) @step(u'I expand the first section$') def i_expand_a_section(step): expand_locator = 'section.courseware-section a.expand' - css_click(expand_locator) + world.css_click(expand_locator) @step(u'I see the "([^"]*)" link$') def i_see_the_span_with_text(step, text): span_locator = '.toggle-button-sections span' - assert_true(world.browser.is_element_present_by_css(span_locator, 5)) - assert_equal(world.browser.find_by_css(span_locator).value, text) - assert_true(world.browser.find_by_css(span_locator).visible) + assert_true(world.is_css_present(span_locator)) + assert_equal(world.css_find(span_locator).value, text) + assert_true(world.css_visible(span_locator)) @step(u'I do not see the "([^"]*)" link$') def i_do_not_see_the_span_with_text(step, text): # Note that the span will exist on the page but not be visible span_locator = '.toggle-button-sections span' - assert_true(world.browser.is_element_present_by_css(span_locator)) - assert_false(world.browser.find_by_css(span_locator).visible) + assert_true(world.is_css_present(span_locator)) + assert_false(world.css_visible(span_locator)) @step(u'all sections are expanded$') def all_sections_are_expanded(step): subsection_locator = 'div.subsection-list' - subsections = world.browser.find_by_css(subsection_locator) + subsections = world.css_find(subsection_locator) for s in subsections: assert_true(s.visible) @@ -111,6 +114,6 @@ def all_sections_are_expanded(step): @step(u'all sections are collapsed$') def all_sections_are_expanded(step): subsection_locator = 'div.subsection-list' - subsections = world.browser.find_by_css(subsection_locator) + subsections = world.css_find(subsection_locator) for s in subsections: assert_false(s.visible) diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 1be5f4aeb9..28285bf8a1 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -3,13 +3,15 @@ Feature: Create Subsection As a course author I want to create and edit subsections - Scenario: Add a new subsection to a section + @skip-phantom + Scenario: Add a new subsection to a section Given I have opened a new course section in Studio When I click the New Subsection link And I enter the subsection name and click save Then I see my subsection on the Courseware page - Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216) + @skip-phantom + Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216) Given I have opened a new course section in Studio When I click the New Subsection link And I enter a subsection name with a quote and click save @@ -17,8 +19,24 @@ Feature: Create Subsection And I click to edit the subsection name Then I see the complete subsection name with a quote in the editor - @skip-phantom - Scenario: Delete a subsection + Scenario: Assign grading type to a subsection and verify it is still shown after refresh (bug #258) + Given I have opened a new course section in Studio + And I have added a new subsection + And I mark it as Homework + Then I see it marked as Homework + And I reload the page + Then I see it marked as Homework + + @skip-phantom + Scenario: Set a due date in a different year (bug #256) + Given I have opened a new subsection in Studio + And I have set a release date and due date in different years + Then I see the correct dates + And I reload the page + Then I see the correct dates + + @skip-phantom + Scenario: Delete a subsection Given I have opened a new course section in Studio And I have added a new subsection And I see my subsection on the Courseware page diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index 88e1424898..f9e5b52bb2 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -1,3 +1,6 @@ +#pylint: disable=C0111 +#pylint: disable=W0621 + from lettuce import world, step from common import * from nose.tools import assert_equal @@ -7,16 +10,27 @@ from nose.tools import assert_equal @step('I have opened a new course section in Studio$') def i_have_opened_a_new_course_section(step): - clear_courses() + world.clear_courses() log_into_studio() create_a_course() add_section() +@step('I have added a new subsection$') +def i_have_added_a_new_subsection(step): + add_subsection() + + +@step('I have opened a new subsection in Studio$') +def i_have_opened_a_new_subsection(step): + step.given('I have opened a new course section in Studio') + step.given('I have added a new subsection') + world.css_click('span.subsection-name-value') + + @step('I click the New Subsection link') def i_click_the_new_subsection_link(step): - css = 'a.new-subsection-item' - css_click(css) + world.css_click('a.new-subsection-item') @step('I enter the subsection name and click save$') @@ -31,19 +45,41 @@ def i_save_subsection_name_with_quote(step): @step('I click to edit the subsection name$') def i_click_to_edit_subsection_name(step): - css_click('span.subsection-name-value') + world.css_click('span.subsection-name-value') @step('I see the complete subsection name with a quote in the editor$') def i_see_complete_subsection_name_with_quote_in_editor(step): css = '.subsection-display-name-input' - assert world.browser.is_element_present_by_css(css, 5) - assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"') + assert world.is_css_present(css) + assert_equal(world.css_find(css).value, 'Subsection With "Quote"') -@step('I have added a new subsection$') -def i_have_added_a_new_subsection(step): - add_subsection() +@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', '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', '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('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('04:00', world.css_find('input#due_time').first.value) + + +@step('I mark it as Homework$') +def i_mark_it_as_homework(step): + world.css_click('a.menu-toggle') + world.browser.click_link_by_text('Homework') + + +@step('I see it marked as Homework$') +def i_see_it_marked__as_homework(step): + assert_equal(world.css_find(".status-label").value, 'Homework') ############ ASSERTIONS ################### @@ -70,11 +106,12 @@ def the_subsection_does_not_exist(step): def save_subsection_name(name): name_css = 'input.new-subsection-name-input' save_css = 'input.new-subsection-name-save' - css_fill(name_css, name) - css_click(save_css) + world.css_fill(name_css, name) + world.css_click(save_css) + def see_subsection_name(name): css = 'span.subsection-name' - assert world.browser.is_element_present_by_css(css) + assert world.is_css_present(css) css = 'span.subsection-name-value' - assert_css_with_text(css, name) + assert world.css_has_text(css, name) diff --git a/cms/djangoapps/contentstore/management/commands/check_course.py b/cms/djangoapps/contentstore/management/commands/check_course.py new file mode 100644 index 0000000000..215bb8add8 --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/check_course.py @@ -0,0 +1,68 @@ +from django.core.management.base import BaseCommand, CommandError +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.xml_importer import check_module_metadata_editability +from xmodule.course_module import CourseDescriptor + +from request_cache.middleware import RequestCache + + +class Command(BaseCommand): + help = '''Enumerates through the course and find common errors''' + + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("check_course requires one argument: ") + + loc_str = args[0] + + loc = CourseDescriptor.id_to_location(loc_str) + store = modulestore() + + # setup a request cache so we don't throttle the DB with all the metadata inheritance requests + store.request_cache = RequestCache.get_request_cache() + + course = store.get_item(loc, depth=3) + + err_cnt = 0 + + def _xlint_metadata(module): + err_cnt = check_module_metadata_editability(module) + for child in module.get_children(): + err_cnt = err_cnt + _xlint_metadata(child) + return err_cnt + + err_cnt = err_cnt + _xlint_metadata(course) + + # we've had a bug where the xml_attributes field can we rewritten as a string rather than a dict + def _check_xml_attributes_field(module): + err_cnt = 0 + if hasattr(module, 'xml_attributes') and isinstance(module.xml_attributes, basestring): + print 'module = {0} has xml_attributes as a string. It should be a dict'.format(module.location.url()) + err_cnt = err_cnt + 1 + for child in module.get_children(): + err_cnt = err_cnt + _check_xml_attributes_field(child) + return err_cnt + + err_cnt = err_cnt + _check_xml_attributes_field(course) + + # check for dangling discussion items, this can cause errors in the forums + def _get_discussion_items(module): + discussion_items = [] + if module.location.category == 'discussion': + discussion_items = discussion_items + [module.location.url()] + + for child in module.get_children(): + discussion_items = discussion_items + _get_discussion_items(child) + + return discussion_items + + discussion_items = _get_discussion_items(course) + + # now query all discussion items via get_items() and compare with the tree-traversal + queried_discussion_items = store.get_items(['i4x', course.location.org, course.location.course, + 'discussion', None, None]) + + for item in queried_discussion_items: + if item.location.url() not in discussion_items: + print 'Found dangling discussion module = {0}'.format(item.location.url()) + diff --git a/cms/djangoapps/contentstore/management/commands/clone.py b/cms/djangoapps/contentstore/management/commands/clone.py index abf04f3da3..0ca50acb50 100644 --- a/cms/djangoapps/contentstore/management/commands/clone.py +++ b/cms/djangoapps/contentstore/management/commands/clone.py @@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.store_utilities import clone_course from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore -from xmodule.modulestore import Location from xmodule.course_module import CourseDescriptor from auth.authz import _copy_course_group @@ -16,8 +15,7 @@ from auth.authz import _copy_course_group class Command(BaseCommand): - help = \ -'''Clone a MongoDB backed course to another location''' + help = 'Clone a MongoDB backed course to another location' def handle(self, *args, **options): if len(args) != 2: diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py index fc92205030..5aafe9f8a6 100644 --- a/cms/djangoapps/contentstore/management/commands/delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/delete_course.py @@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore -from xmodule.modulestore import Location from xmodule.course_module import CourseDescriptor from .prompt import query_yes_no @@ -38,7 +37,7 @@ class Command(BaseCommand): if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"): if query_yes_no("Are you sure. This action cannot be undone!", default="no"): loc = CourseDescriptor.id_to_location(loc_str) - if delete_course(ms, cs, loc, commit) == True: + if delete_course(ms, cs, loc, commit): print 'removing User permissions from course....' # in the django layer, we need to remove all the user permissions groups associated with this course if commit: diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index 11b043c2ab..eb7800d46c 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore -from xmodule.modulestore import Location from xmodule.course_module import CourseDescriptor @@ -15,8 +14,7 @@ unnamed_modules = 0 class Command(BaseCommand): - help = \ -'''Import the specified data directory into the default ModuleStore''' + help = 'Import the specified data directory into the default ModuleStore' def handle(self, *args, **options): if len(args) != 2: diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 2a040f35b6..9b919daad0 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -12,8 +12,7 @@ unnamed_modules = 0 class Command(BaseCommand): - help = \ -'''Import the specified data directory into the default ModuleStore''' + help = 'Import the specified data directory into the default ModuleStore' def handle(self, *args, **options): if len(args) == 0: @@ -28,4 +27,4 @@ class Command(BaseCommand): data=data_dir, courses=course_dirs) import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False, - static_content_store=contentstore(), verbose=True) + static_content_store=contentstore(), verbose=True) diff --git a/cms/djangoapps/contentstore/management/commands/prompt.py b/cms/djangoapps/contentstore/management/commands/prompt.py index 40a39d0a11..44f981b5ac 100644 --- a/cms/djangoapps/contentstore/management/commands/prompt.py +++ b/cms/djangoapps/contentstore/management/commands/prompt.py @@ -11,8 +11,8 @@ def query_yes_no(question, default="yes"): The "answer" return value is one of "yes" or "no". """ - valid = {"yes":True, "y":True, "ye":True, - "no":False, "n":False} + valid = {"yes": True, "y": True, "ye": True, + "no": False, "n": False} if default is None: prompt = " [y/n] " elif default == "yes": @@ -30,5 +30,4 @@ def query_yes_no(question, default="yes"): elif choice in valid: return valid[choice] else: - sys.stdout.write("Please respond with 'yes' or 'no' "\ - "(or 'y' or 'n').\n") + sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n") diff --git a/cms/djangoapps/contentstore/management/commands/update_templates.py b/cms/djangoapps/contentstore/management/commands/update_templates.py index b30d30480a..e94fee64b8 100644 --- a/cms/djangoapps/contentstore/management/commands/update_templates.py +++ b/cms/djangoapps/contentstore/management/commands/update_templates.py @@ -1,9 +1,9 @@ from xmodule.templates import update_templates from django.core.management.base import BaseCommand + class Command(BaseCommand): - help = \ -'''Imports and updates the Studio component templates from the code pack and put in the DB''' + help = 'Imports and updates the Studio component templates from the code pack and put in the DB' def handle(self, *args, **options): - update_templates() \ No newline at end of file + update_templates() diff --git a/cms/djangoapps/contentstore/management/commands/xlint.py b/cms/djangoapps/contentstore/management/commands/xlint.py index 6bc254a1ff..21c8e7d1f8 100644 --- a/cms/djangoapps/contentstore/management/commands/xlint.py +++ b/cms/djangoapps/contentstore/management/commands/xlint.py @@ -1,7 +1,5 @@ from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.xml_importer import perform_xlint -from xmodule.modulestore.django import modulestore -from xmodule.contentstore.django import contentstore unnamed_modules = 0 @@ -9,10 +7,11 @@ unnamed_modules = 0 class Command(BaseCommand): help = \ - ''' - Verify the structure of courseware as to it's suitability for import - To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)] - ''' + ''' + Verify the structure of courseware as to it's suitability for import + To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)] + ''' + def handle(self, *args, **options): if len(args) == 0: raise CommandError("import requires at least one argument: [...]") diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py index 8ea6add88d..91f722a699 100644 --- a/cms/djangoapps/contentstore/module_info_model.py +++ b/cms/djangoapps/contentstore/module_info_model.py @@ -1,7 +1,6 @@ from static_replace import replace_static_urls from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore import Location -from django.http import Http404 def get_module_info(store, location, parent_location=None, rewrite_static_links=False): diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 615ffb6ed0..07b7032e60 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -6,16 +6,17 @@ from django.conf import settings from django.core.urlresolvers import reverse from path import path from tempdir import mkdtemp_clean -from datetime import timedelta -import json from fs.osfs import OSFS import copy from json import loads +from datetime import timedelta from django.contrib.auth.models import User +from django.dispatch import Signal from contentstore.utils import get_modulestore +from contentstore.tests.utils import parse_json -from .utils import ModuleStoreTestCase, parse_json +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore import Location @@ -25,7 +26,7 @@ from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore from xmodule.templates import update_templates from xmodule.modulestore.xml_exporter import export_to_xml -from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint from xmodule.modulestore.inheritance import own_metadata from xmodule.capa_module import CapaDescriptor @@ -38,6 +39,16 @@ TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data' TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') +class MongoCollectionFindWrapper(object): + def __init__(self, original): + self.original = original + self.counter = 0 + + def find(self, query, *args, **kwargs): + self.counter = self.counter+1 + return self.original(query, *args, **kwargs) + + @override_settings(MODULESTORE=TEST_DATA_MODULESTORE) class ContentStoreToyCourseTest(ModuleStoreTestCase): """ @@ -77,6 +88,138 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): def test_edit_unit_full(self): self.check_edit_unit('full') + def _get_draft_counts(self, item): + cnt = 1 if getattr(item, 'is_draft', False) else 0 + for child in item.get_children(): + cnt = cnt + self._get_draft_counts(child) + + return cnt + + def test_get_items(self): + ''' + This verifies a bug we had where the None setting in get_items() meant 'wildcard' + Unfortunately, None = published for the revision field, so get_items() would return + both draft and non-draft copies. + ''' + store = modulestore() + draft_store = modulestore('draft') + import_from_xml(store, 'common/test/data/', ['simple']) + + html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) + + draft_store.clone_item(html_module.location, html_module.location) + + # now query get_items() to get this location with revision=None, this should just + # return back a single item (not 2) + + items = store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None]) + self.assertEqual(len(items), 1) + self.assertFalse(getattr(items[0], 'is_draft', False)) + + # now refetch from the draft store. Note that even though we pass + # None in the revision field, the draft store will replace that with 'draft' + items = draft_store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None]) + self.assertEqual(len(items), 1) + self.assertTrue(getattr(items[0], 'is_draft', False)) + + 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']) + + course = modulestore('draft').get_item( + Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]), + depth=None + ) + + # make sure no draft items have been returned + num_drafts = self._get_draft_counts(course) + self.assertEqual(num_drafts, 0) + + problem = modulestore('draft').get_item( + Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]) + ) + + # put into draft + modulestore('draft').clone_item(problem.location, problem.location) + + # make sure we can query that item and verify that it is a draft + draft_problem = modulestore('draft').get_item( + Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]) + ) + self.assertTrue(getattr(draft_problem, 'is_draft', False)) + + #now requery with depth + course = modulestore('draft').get_item( + Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]), + depth=None + ) + + # make sure just one draft item have been returned + num_drafts = self._get_draft_counts(course) + self.assertEqual(num_drafts, 1) + def test_static_tab_reordering(self): import_from_xml(modulestore(), 'common/test/data/', ['full']) @@ -107,46 +250,50 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): module_store = modulestore('direct') found = False - item = None items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None]) found = len(items) > 0 self.assertTrue(found) # check that there's actually content in the 'question' field - self.assertGreater(len(items[0].question),0) + self.assertGreater(len(items[0].question), 0) + + def test_xlint_fails(self): + err_cnt = perform_xlint('common/test/data', ['full']) + self.assertGreater(err_cnt, 0) def test_delete(self): import_from_xml(modulestore(), 'common/test/data/', ['full']) - module_store = modulestore('direct') + direct_store = modulestore('direct') - sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None])) + sequential = direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None])) - chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None])) + chapter = direct_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None])) - # make sure the parent no longer points to the child object which was deleted + # make sure the parent points to the child object which is to be deleted self.assertTrue(sequential.location.url() in chapter.children) - self.client.post(reverse('delete_item'), + self.client.post( + reverse('delete_item'), json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}), - "application/json") + "application/json" + ) found = False try: - module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None])) + direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None])) found = True except ItemNotFoundError: pass self.assertFalse(found) - chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None])) + chapter = direct_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None])) # make sure the parent no longer points to the child object which was deleted self.assertFalse(sequential.location.url() in chapter.children) - def test_about_overrides(self): ''' This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html @@ -165,7 +312,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): import_from_xml(modulestore(), 'common/test/data/', ['full']) module_store = modulestore('direct') - content_store = contentstore() source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') course = module_store.get_item(source_location) @@ -178,7 +324,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): 'org': 'MITx', 'number': '999', 'display_name': 'Robot Super Course', - } + } import_from_xml(modulestore(), 'common/test/data/', ['full']) @@ -207,6 +353,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()})) self.assertEqual(resp.status_code, 200) + def test_bad_contentstore_request(self): + resp = self.client.get('http://localhost:8001/c4x/CDX/123123/asset/&images_circuits_Lab7Solution2.png') + self.assertEqual(resp.status_code, 400) + def test_delete_course(self): import_from_xml(modulestore(), 'common/test/data/', ['full']) @@ -233,17 +383,48 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): def test_export_course(self): module_store = modulestore('direct') + draft_store = modulestore('draft') content_store = contentstore() import_from_xml(module_store, 'common/test/data/', ['full']) location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') + # get a vertical (and components in it) to put into 'draft' + vertical = module_store.get_item(Location(['i4x', 'edX', 'full', + 'vertical', 'vertical_66', None]), depth=1) + + draft_store.clone_item(vertical.location, vertical.location) + + # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case. + draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'full', + 'vertical', 'no_references', 'draft'])) + + for child in vertical.get_children(): + draft_store.clone_item(child.location, child.location) + root_dir = path(mkdtemp_clean()) + # now create a private vertical + private_vertical = draft_store.clone_item(vertical.location, + Location(['i4x', 'edX', 'full', 'vertical', 'a_private_vertical', None])) + + # add private to list of children + sequential = module_store.get_item(Location(['i4x', 'edX', 'full', + 'sequential', 'Administrivia_and_Circuit_Elements', None])) + private_location_no_draft = private_vertical.location._replace(revision=None) + module_store.update_children(sequential.location, sequential.children + + [private_location_no_draft.url()]) + + # read back the sequential, to make sure we have a pointer to + sequential = module_store.get_item(Location(['i4x', 'edX', 'full', + 'sequential', 'Administrivia_and_Circuit_Elements', None])) + + self.assertIn(private_location_no_draft.url(), sequential.children) + print 'Exporting to tempdir = {0}'.format(root_dir) # export out to a tempdir - export_to_xml(module_store, content_store, location, root_dir, 'test_export') + export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store) # check for static tabs self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html') @@ -277,20 +458,42 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): delete_course(module_store, content_store, location) # reimport - import_from_xml(module_store, root_dir, ['test_export']) + import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store) items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) self.assertGreater(len(items), 0) for descriptor in items: - print "Checking {0}....".format(descriptor.location.url()) - resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) - self.assertEqual(resp.status_code, 200) + # don't try to look at private verticals. Right now we're running + # the service in non-draft aware + if getattr(descriptor, 'is_draft', False): + print "Checking {0}....".format(descriptor.location.url()) + resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) + self.assertEqual(resp.status_code, 200) + + # verify that we have the content in the draft store as well + vertical = draft_store.get_item(Location(['i4x', 'edX', 'full', + 'vertical', 'vertical_66', None]), depth=1) + + self.assertTrue(getattr(vertical, 'is_draft', False)) + for child in vertical.get_children(): + self.assertTrue(getattr(child, 'is_draft', False)) + + # make sure that we don't have a sequential that is in draft mode + sequential = draft_store.get_item(Location(['i4x', 'edX', 'full', + 'sequential', 'Administrivia_and_Circuit_Elements', None])) + + self.assertFalse(getattr(sequential, 'is_draft', False)) + + # verify that we have the private vertical + test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full', + 'vertical', 'vertical_66', None])) + + self.assertTrue(getattr(test_private_vertical, 'is_draft', False)) shutil.rmtree(root_dir) def test_course_handouts_rewrites(self): module_store = modulestore('direct') - content_store = contentstore() # import a test course import_from_xml(module_store, 'common/test/data/', ['full']) @@ -307,6 +510,28 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # note, we know the link it should be because that's what in the 'full' course in the test data self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf') + def test_prefetch_children(self): + import_from_xml(modulestore(), 'common/test/data/', ['full']) + module_store = modulestore('direct') + location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') + + wrapper = MongoCollectionFindWrapper(module_store.collection.find) + module_store.collection.find = wrapper.find + course = module_store.get_item(location, depth=2) + + # make sure we haven't done too many round trips to DB + # note we say 4 round trips here for 1) the course, 2 & 3) for the chapters and sequentials, and + # 4) because of the RT due to calculating the inherited metadata + self.assertEqual(wrapper.counter, 4) + + # make sure we pre-fetched a known sequential which should be at depth=2 + self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential', + 'Administrivia_and_Circuit_Elements', None]) in course.system.module_data) + + # make sure we don't have a specific vertical which should be at depth=3 + self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', None]) + in course.system.module_data) + def test_export_course_with_unknown_metadata(self): module_store = modulestore('direct') content_store = contentstore() @@ -327,14 +552,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): print 'Exporting to tempdir = {0}'.format(root_dir) # export out to a tempdir - exported = False - try: - export_to_xml(module_store, content_store, location, root_dir, 'test_export') - exported = True - except Exception: - pass + export_to_xml(module_store, content_store, location, root_dir, 'test_export') - self.assertTrue(exported) class ContentStoreTest(ModuleStoreTestCase): """ @@ -370,7 +589,7 @@ class ContentStoreTest(ModuleStoreTestCase): 'org': 'MITx', 'number': '999', 'display_name': 'Robot Super Course', - } + } def test_create_course(self): """Test new course creation - happy path""" @@ -397,7 +616,7 @@ class ContentStoreTest(ModuleStoreTestCase): self.assertEqual(resp.status_code, 200) self.assertEqual(data['ErrMsg'], - 'There is already a course defined with the same organization and course number.') + 'There is already a course defined with the same organization and course number.') def test_create_course_with_bad_organization(self): """Test new course creation - error path for bad organization name""" @@ -407,16 +626,18 @@ class ContentStoreTest(ModuleStoreTestCase): self.assertEqual(resp.status_code, 200) self.assertEqual(data['ErrMsg'], - "Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.") + "Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.") def test_course_index_view_with_no_courses(self): """Test viewing the index page with no courses""" # Create a course so there is something to view resp = self.client.get(reverse('index')) - self.assertContains(resp, + self.assertContains( + resp, '

My Courses

', status_code=200, - html=True) + html=True + ) def test_course_factory(self): """Test that the course factory works correctly.""" @@ -433,26 +654,30 @@ class ContentStoreTest(ModuleStoreTestCase): """Test viewing the index page with an existing course""" CourseFactory.create(display_name='Robot Super Educational Course') resp = self.client.get(reverse('index')) - self.assertContains(resp, + self.assertContains( + resp, 'Robot Super Educational Course', status_code=200, - html=True) + html=True + ) def test_course_overview_view_with_course(self): """Test viewing the course overview page with an existing course""" CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') data = { - 'org': 'MITx', - 'course': '999', - 'name': Location.clean('Robot Super Course'), - } + 'org': 'MITx', + 'course': '999', + 'name': Location.clean('Robot Super Course'), + } resp = self.client.get(reverse('course_index', kwargs=data)) - self.assertContains(resp, + self.assertContains( + resp, '
', status_code=200, - html=True) + html=True + ) def test_clone_item(self): """Test cloning an item. E.g. creating a new section""" @@ -462,14 +687,16 @@ class ContentStoreTest(ModuleStoreTestCase): 'parent_location': 'i4x://MITx/999/course/Robot_Super_Course', 'template': 'i4x://edx/templates/chapter/Empty', 'display_name': 'Section One', - } + } resp = self.client.post(reverse('clone_item'), section_data) self.assertEqual(resp.status_code, 200) data = parse_json(resp) - self.assertRegexpMatches(data['id'], - '^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$') + self.assertRegexpMatches( + data['id'], + r"^i4x://MITx/999/chapter/([0-9]|[a-f]){32}$" + ) def test_capa_module(self): """Test that a problem treats markdown specially.""" @@ -478,7 +705,7 @@ class ContentStoreTest(ModuleStoreTestCase): problem_data = { 'parent_location': 'i4x://MITx/999/course/Robot_Super_Course', 'template': 'i4x://edx/templates/problem/Blank_Common_Problem' - } + } resp = self.client.post(reverse('clone_item'), problem_data) @@ -492,6 +719,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') @@ -505,6 +839,45 @@ class ContentStoreTest(ModuleStoreTestCase): # make sure we found the item (e.g. it didn't error while loading) self.assertTrue(did_load_item) + def test_forum_id_generation(self): + import_from_xml(modulestore(), 'common/test/data/', ['full']) + module_store = modulestore('direct') + new_component_location = Location('i4x', 'edX', 'full', 'discussion', 'new_component') + source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag') + + # crate a new module and add it as a child to a vertical + module_store.clone_item(source_template_location, new_component_location) + + new_discussion_item = module_store.get_item(new_component_location) + + self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$') + + def test_update_modulestore_signal_did_fire(self): + + import_from_xml(modulestore(), 'common/test/data/', ['full']) + module_store = modulestore('direct') + + try: + module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location']) + + self.got_signal = False + + def _signal_hander(modulestore=None, course_id=None, location=None, **kwargs): + self.got_signal = True + + module_store.modulestore_update_signal.connect(_signal_hander) + + new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component') + source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page') + + # crate a new module + module_store.clone_item(source_template_location, new_component_location) + + finally: + module_store.modulestore_update_signal = None + + self.assertTrue(self.got_signal) + def test_metadata_inheritance(self): import_from_xml(modulestore(), 'common/test/data/', ['full']) @@ -528,7 +901,7 @@ class ContentStoreTest(ModuleStoreTestCase): module_store.update_children(parent.location, parent.children + [new_component_location.url()]) # flush the cache - module_store.get_cached_metadata_inheritance_tree(new_component_location, -1) + module_store.refresh_cached_metadata_inheritance_tree(new_component_location) new_module = module_store.get_item(new_component_location) # check for grace period definition which should be defined at the course level @@ -543,7 +916,7 @@ class ContentStoreTest(ModuleStoreTestCase): module_store.update_metadata(new_module.location, own_metadata(new_module)) # flush the cache and refetch - module_store.get_cached_metadata_inheritance_tree(new_component_location, -1) + module_store.refresh_cached_metadata_inheritance_tree(new_component_location) new_module = module_store.get_item(new_component_location) self.assertEqual(timedelta(1), new_module.lms.graceperiod) diff --git a/cms/djangoapps/contentstore/tests/test_core_caching.py b/cms/djangoapps/contentstore/tests/test_core_caching.py index 676627a045..34ed24699d 100644 --- a/cms/djangoapps/contentstore/tests/test_core_caching.py +++ b/cms/djangoapps/contentstore/tests/test_core_caching.py @@ -23,14 +23,14 @@ class CachingTestCase(TestCase): def test_put_and_get(self): set_cached_content(self.mockAsset) self.assertEqual(self.mockAsset.content, get_cached_content(self.unicodeLocation).content, - 'should be stored in cache with unicodeLocation') + 'should be stored in cache with unicodeLocation') self.assertEqual(self.mockAsset.content, get_cached_content(self.nonUnicodeLocation).content, - 'should be stored in cache with nonUnicodeLocation') + 'should be stored in cache with nonUnicodeLocation') def test_delete(self): set_cached_content(self.mockAsset) del_cached_content(self.nonUnicodeLocation) self.assertEqual(None, get_cached_content(self.unicodeLocation), - 'should not be stored in cache with unicodeLocation') + 'should not be stored in cache with unicodeLocation') self.assertEqual(None, get_cached_content(self.nonUnicodeLocation), - 'should not be stored in cache with nonUnicodeLocation') + 'should not be stored in cache with nonUnicodeLocation') diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 2e7bc5db83..c9f6b2053e 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -1,8 +1,6 @@ import datetime import json import copy -from util import converters -from util.converters import jsdate_to_time from django.contrib.auth.models import User from django.test.client import Client @@ -10,73 +8,17 @@ from django.core.urlresolvers import reverse from django.utils.timezone import UTC from xmodule.modulestore import Location -from models.settings.course_details import (CourseDetails, - CourseSettingsEncoder) +from models.settings.course_details import (CourseDetails, CourseSettingsEncoder) from models.settings.course_grading import CourseGradingModel from contentstore.utils import get_modulestore -from django.test import TestCase -from .utils import ModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from models.settings.course_metadata import CourseMetadata from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.django import modulestore -import time - - -# YYYY-MM-DDThh:mm:ss.s+/-HH:MM -class ConvertersTestCase(TestCase): - @staticmethod - def struct_to_datetime(struct_time): - return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, - struct_time.tm_mday, struct_time.tm_hour, - struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC()) - - def compare_dates(self, date1, date2, expected_delta): - dt1 = ConvertersTestCase.struct_to_datetime(date1) - dt2 = ConvertersTestCase.struct_to_datetime(date2) - self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" - + str(date2) + "!=" + str(expected_delta)) - - def test_iso_to_struct(self): - '''Test conversion from iso compatible date strings to struct_time''' - self.compare_dates(converters.jsdate_to_time("2013-01-01"), - converters.jsdate_to_time("2012-12-31"), - datetime.timedelta(days=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00"), - converters.jsdate_to_time("2012-12-31T23"), - datetime.timedelta(hours=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), - converters.jsdate_to_time("2012-12-31T23:59"), - datetime.timedelta(minutes=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), - converters.jsdate_to_time("2012-12-31T23:59:59"), - datetime.timedelta(seconds=1)) - self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00Z"), - converters.jsdate_to_time("2012-12-31T23:59:59Z"), - datetime.timedelta(seconds=1)) - self.compare_dates( - converters.jsdate_to_time("2012-12-31T23:00:01-01:00"), - converters.jsdate_to_time("2013-01-01T00:00:00+01:00"), - datetime.timedelta(hours=1, seconds=1)) - - def test_struct_to_iso(self): - ''' - Test converting time reprs to iso dates - ''' - self.assertEqual( - converters.time_to_isodate( - time.strptime("2012-12-31T23:59:59Z", "%Y-%m-%dT%H:%M:%SZ")), - "2012-12-31T23:59:59Z") - self.assertEqual( - converters.time_to_isodate( - jsdate_to_time("2012-12-31T23:59:59Z")), - "2012-12-31T23:59:59Z") - self.assertEqual( - converters.time_to_isodate( - jsdate_to_time("2012-12-31T23:00:01-01:00")), - "2013-01-01T00:00:01Z") +from xmodule.fields import Date class CourseTestCase(ModuleStoreTestCase): @@ -105,12 +47,8 @@ class CourseTestCase(ModuleStoreTestCase): self.client = Client() self.client.login(username=uname, password=password) - t = 'i4x://edx/templates/course/Empty' - o = 'MITx' - n = '999' - dn = 'Robot Super Course' - self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course') - CourseFactory.create(template=t, org=o, number=n, display_name=dn) + course = CourseFactory.create(template='i4x://edx/templates/course/Empty', org='MITx', number='999', display_name='Robot Super Course') + self.course_location = course.location class CourseDetailsTestCase(CourseTestCase): @@ -144,17 +82,25 @@ class CourseDetailsTestCase(CourseTestCase): jsondetails = CourseDetails.fetch(self.course_location) jsondetails.syllabus = "bar" # encode - decode to convert date fields and other data which changes form - self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus, - jsondetails.syllabus, "After set syllabus") + self.assertEqual( + CourseDetails.update_from_json(jsondetails.__dict__).syllabus, + jsondetails.syllabus, "After set syllabus" + ) jsondetails.overview = "Overview" - self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).overview, - jsondetails.overview, "After set overview") + self.assertEqual( + CourseDetails.update_from_json(jsondetails.__dict__).overview, + jsondetails.overview, "After set overview" + ) jsondetails.intro_video = "intro_video" - self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).intro_video, - jsondetails.intro_video, "After set intro_video") + self.assertEqual( + CourseDetails.update_from_json(jsondetails.__dict__).intro_video, + jsondetails.intro_video, "After set intro_video" + ) jsondetails.effort = "effort" - self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort, - jsondetails.effort, "After set effort") + self.assertEqual( + CourseDetails.update_from_json(jsondetails.__dict__).effort, + jsondetails.effort, "After set effort" + ) class CourseDetailsViewTest(CourseTestCase): @@ -206,17 +152,22 @@ class CourseDetailsViewTest(CourseTestCase): self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==") + @staticmethod + def struct_to_datetime(struct_time): + return datetime.datetime(*struct_time[:6], tzinfo=UTC()) + def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: + date = Date() if field in encoded and encoded[field] is not None: - encoded_encoded = jsdate_to_time(encoded[field]) - dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded) + encoded_encoded = date.from_json(encoded[field]) + dt1 = CourseDetailsViewTest.struct_to_datetime(encoded_encoded) if isinstance(details[field], datetime.datetime): dt2 = details[field] else: - details_encoded = jsdate_to_time(details[field]) - dt2 = ConvertersTestCase.struct_to_datetime(details_encoded) + details_encoded = date.from_json(details[field]) + dt2 = CourseDetailsViewTest.struct_to_datetime(details_encoded) expected_delta = datetime.timedelta(0) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) @@ -300,6 +251,7 @@ class CourseGradingTest(CourseTestCase): altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2") + class CourseMetadataEditingTest(CourseTestCase): def setUp(self): CourseTestCase.setUp(self) @@ -307,7 +259,6 @@ class CourseMetadataEditingTest(CourseTestCase): import_from_xml(modulestore(), 'common/test/data/', ['full']) self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]) - def test_fetch_initial_fields(self): test_model = CourseMetadata.fetch(self.course_location) self.assertIn('display_name', test_model, 'Missing editable metadata field') @@ -322,18 +273,20 @@ class CourseMetadataEditingTest(CourseTestCase): self.assertIn('xqa_key', test_model, 'xqa_key field ') def test_update_from_json(self): - test_model = CourseMetadata.update_from_json(self.course_location, - { "advertised_start" : "start A", - "testcenter_info" : { "c" : "test" }, - "days_early_for_beta" : 2}) + test_model = CourseMetadata.update_from_json(self.course_location, { + "advertised_start": "start A", + "testcenter_info": {"c": "test"}, + "days_early_for_beta": 2 + }) self.update_check(test_model) # try fresh fetch to ensure persistence test_model = CourseMetadata.fetch(self.course_location) self.update_check(test_model) # now change some of the existing metadata - test_model = CourseMetadata.update_from_json(self.course_location, - { "advertised_start" : "start B", - "display_name" : "jolly roger"}) + test_model = CourseMetadata.update_from_json(self.course_location, { + "advertised_start": "start B", + "display_name": "jolly roger"} + ) self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value") self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field') @@ -345,13 +298,12 @@ class CourseMetadataEditingTest(CourseTestCase): self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field') self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value") self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field') - self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value") + self.assertDictEqual(test_model['testcenter_info'], {"c": "test"}, "testcenter_info not expected value") self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field') self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value") - def test_delete_key(self): - test_model = CourseMetadata.delete_key(self.fullcourse_location, { 'deleteKeys' : ['doesnt_exist', 'showanswer', 'xqa_key']}) + test_model = CourseMetadata.delete_key(self.fullcourse_location, {'deleteKeys': ['doesnt_exist', 'showanswer', 'xqa_key']}) # ensure no harm self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in') self.assertIn('display_name', test_model, 'full missing editable metadata field') diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py new file mode 100644 index 0000000000..e6d68ba004 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -0,0 +1,97 @@ +from unittest import skip + +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from django.test.client import Client + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + +class InternationalizationTest(ModuleStoreTestCase): + """ + Tests to validate Internationalization. + """ + + def setUp(self): + """ + These tests need a user in the DB so that the django Test Client + can log them in. + They inherit from the ModuleStoreTestCase class so that the mongodb collection + will be cleared out before each test case execution and deleted + afterwards. + """ + self.uname = 'testuser' + self.email = 'test+courses@edx.org' + self.password = 'foo' + + # Create the use so we can log them in. + self.user = User.objects.create_user(self.uname, self.email, self.password) + + # Note that we do not actually need to do anything + # for registration if we directly mark them active. + self.user.is_active = True + # Staff has access to view all courses + self.user.is_staff = True + self.user.save() + + self.course_data = { + 'template': 'i4x://edx/templates/course/Empty', + 'org': 'MITx', + 'number': '999', + 'display_name': 'Robot Super Course', + } + + def test_course_plain_english(self): + """Test viewing the index page with no courses""" + self.client = Client() + self.client.login(username=self.uname, password=self.password) + + resp = self.client.get(reverse('index')) + self.assertContains(resp, + '

My Courses

', + status_code=200, + html=True) + + def test_course_explicit_english(self): + """Test viewing the index page with no courses""" + self.client = Client() + self.client.login(username=self.uname, password=self.password) + + resp = self.client.get(reverse('index'), + {}, + HTTP_ACCEPT_LANGUAGE='en' + ) + + self.assertContains(resp, + '

My Courses

', + status_code=200, + html=True) + + + # **** + # NOTE: + # **** + # + # This test will break when we replace this fake 'test' language + # with actual French. This test will need to be updated with + # actual French at that time. + + # Test temporarily disable since it depends on creation of dummy strings + @skip + def test_course_with_accents (self): + """Test viewing the index page with no courses""" + self.client = Client() + self.client.login(username=self.uname, password=self.password) + + resp = self.client.get(reverse('index'), + {}, + HTTP_ACCEPT_LANGUAGE='fr' + ) + + TEST_STRING = u'

' \ + + u'My \xc7\xf6\xfcrs\xe9s L#' \ + + u'

' + + self.assertContains(resp, + TEST_STRING, + status_code=200, + html=True) diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index bbaebfb687..eb7bfb6db9 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -3,7 +3,7 @@ from contentstore import utils import mock from django.test import TestCase from xmodule.modulestore.tests.factories import CourseFactory -from .utils import ModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase class LMSLinksTestCase(TestCase): @@ -30,7 +30,7 @@ class LMSLinksTestCase(TestCase): class UrlReverseTestCase(ModuleStoreTestCase): """ Tests for get_url_reverse """ - def test_CoursePageNames(self): + def test_course_page_names(self): """ Test the defined course pages. """ course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course') @@ -69,4 +69,4 @@ class UrlReverseTestCase(ModuleStoreTestCase): self.assertEquals( 'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course) - ) \ No newline at end of file + ) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index e43a95fccd..34e5da4b4d 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -1,30 +1,8 @@ -import json -import shutil from django.test.client import Client -from django.conf import settings from django.core.urlresolvers import reverse -from path import path -import json -from fs.osfs import OSFS -import copy -from contentstore.utils import get_modulestore - -from xmodule.modulestore import Location -from xmodule.modulestore.store_utilities import clone_course -from xmodule.modulestore.store_utilities import delete_course -from xmodule.modulestore.django import modulestore, _MODULESTORES -from xmodule.contentstore.django import contentstore -from xmodule.templates import update_templates -from xmodule.modulestore.xml_exporter import export_to_xml -from xmodule.modulestore.xml_importer import import_from_xml - -from xmodule.capa_module import CapaDescriptor -from xmodule.course_module import CourseDescriptor -from xmodule.seq_module import SequenceDescriptor - -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from .utils import ModuleStoreTestCase, parse_json, user, registration +from .utils import parse_json, user, registration +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase class ContentStoreTestCase(ModuleStoreTestCase): @@ -84,6 +62,7 @@ class ContentStoreTestCase(ModuleStoreTestCase): # Now make sure that the user is now actually activated self.assertTrue(user(email).is_active) + class AuthTestCase(ContentStoreTestCase): """Check that various permissions-related things work""" @@ -101,9 +80,9 @@ class AuthTestCase(ContentStoreTestCase): def test_public_pages_load(self): """Make sure pages that don't require login load without error.""" pages = ( - reverse('login'), - reverse('signup'), - ) + reverse('login'), + reverse('signup'), + ) for page in pages: print "Checking '{0}'".format(page) self.check_page_get(page, 200) @@ -136,13 +115,13 @@ class AuthTestCase(ContentStoreTestCase): """Make sure pages that do require login work.""" auth_pages = ( reverse('index'), - ) + ) # These are pages that should just load when the user is logged in # (no data needed) simple_auth_pages = ( reverse('index'), - ) + ) # need an activated user self.test_create_account() diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index b6b8cd5023..3135e49a08 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -1,54 +1,12 @@ +''' +Utilities for contentstore tests +''' + import json -import copy -from uuid import uuid4 -from django.test import TestCase -from django.conf import settings from student.models import Registration from django.contrib.auth.models import User -import xmodule.modulestore.django -from xmodule.templates import update_templates - - -class ModuleStoreTestCase(TestCase): - """ Subclass for any test case that uses the mongodb - module store. This populates a uniquely named modulestore - collection with templates before running the TestCase - and drops it they are finished. """ - - def _pre_setup(self): - super(ModuleStoreTestCase, self)._pre_setup() - - # Use a uuid to differentiate - # the mongo collections on jenkins. - self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE) - self.test_MODULESTORE = self.orig_MODULESTORE - self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex - self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex - settings.MODULESTORE = self.test_MODULESTORE - - # Flush and initialize the module store - # It needs the templates because it creates new records - # by cloning from the template. - # Note that if your test module gets in some weird state - # (though it shouldn't), do this manually - # from the bash shell to drop it: - # $ mongo test_xmodule --eval "db.dropDatabase()" - xmodule.modulestore.django._MODULESTORES = {} - update_templates() - - def _post_teardown(self): - # Make sure you flush out the modulestore. - # Drop the collection at the end of the test, - # otherwise there will be lingering collections leftover - # from executing the tests. - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - settings.MODULESTORE = self.orig_MODULESTORE - - super(ModuleStoreTestCase, self)._post_teardown() - def parse_json(response): """Parse response, which is assumed to be json""" diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 63dfe5bf5f..a5a3b47bce 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -3,9 +3,13 @@ from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError from django.core.urlresolvers import reverse +import copy DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] +#In order to instantiate an open ended tab automatically, need to have this data +OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"} + def get_modulestore(location): """ @@ -82,11 +86,10 @@ def get_lms_link_for_item(location, preview=False, course_id=None): if settings.LMS_BASE is not None: if preview: - lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', - 'preview.' + settings.LMS_BASE) + lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', 'preview.' + settings.LMS_BASE) else: lms_base = settings.LMS_BASE - + lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format( lms_base=lms_base, course_id=course_id, @@ -137,7 +140,7 @@ def compute_unit_state(unit): 'private' content is editabled and not visible in the LMS """ - if unit.cms.is_draft: + if getattr(unit, 'is_draft', False): try: modulestore('direct').get_item(unit.location) return UnitState.draft @@ -147,10 +150,6 @@ def compute_unit_state(unit): return UnitState.public -def get_date_display(date): - return date.strftime("%d %B, %Y at %I:%M %p") - - def update_item(location, value): """ If value is None, delete the db entry. Otherwise, update it using the correct modulestore. @@ -191,3 +190,37 @@ class CoursePageNames: SettingsGrading = "settings_grading" CourseOutline = "course_index" Checklists = "checklists" + + +def add_open_ended_panel_tab(course): + """ + Used to add the open ended panel tab to a course if it does not exist. + @param course: A course object from the modulestore. + @return: Boolean indicating whether or not a tab was added and a list of tabs for the course. + """ + #Copy course tabs + course_tabs = copy.copy(course.tabs) + changed = False + #Check to see if open ended panel is defined in the course + if OPEN_ENDED_PANEL not in course_tabs: + #Add panel to the tabs if it is not defined + course_tabs.append(OPEN_ENDED_PANEL) + changed = True + return changed, course_tabs + + +def remove_open_ended_panel_tab(course): + """ + Used to remove the open ended panel tab from a course if it exists. + @param course: A course object from the modulestore. + @return: Boolean indicating whether or not a tab was added and a list of tabs for the course. + """ + #Copy course tabs + course_tabs = copy.copy(course.tabs) + changed = False + #Check to see if open ended panel is defined in the course + if OPEN_ENDED_PANEL in course_tabs: + #Add panel to the tabs if it is not defined + course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL] + changed = True + return changed, course_tabs diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 561708c833..caf3901e03 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -6,7 +6,6 @@ import sys import time import tarfile import shutil -from datetime import datetime from collections import defaultdict from uuid import uuid4 from path import path @@ -15,9 +14,6 @@ from tempfile import mkdtemp from django.core.servers.basehttp import FileWrapper from django.core.files.temp import NamedTemporaryFile -# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' -from PIL import Image - from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError from django.http import HttpResponseNotFound from django.contrib.auth.decorators import login_required @@ -42,17 +38,19 @@ from xmodule.modulestore.mongo import MongoUsage from mitxmako.shortcuts import render_to_response, render_to_string from xmodule.modulestore.django import modulestore from xmodule_modifiers import replace_static_urls, wrap_xmodule -from xmodule.exceptions import NotFoundError +from xmodule.exceptions import NotFoundError, ProcessingError from functools import partial from xmodule.contentstore.django import contentstore from xmodule.contentstore.content import StaticContent +from xmodule.util.date_utils import get_default_time_display from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \ - get_date_display, UnitState, get_course_for_item, get_url_reverse + UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \ + remove_open_ended_panel_tab from xmodule.modulestore.xml_importer import import_from_xml from contentstore.course_info_model import get_course_updates, \ @@ -73,7 +71,8 @@ log = logging.getLogger(__name__) COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] -ADVANCED_COMPONENT_TYPES = ['annotatable', 'combinedopenended', 'peergrading'] +OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] +ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' @@ -118,6 +117,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 ================================== @@ -188,7 +193,7 @@ def course_index(request, org, course, name): 'coursename': name }) - course = modulestore().get_item(location) + course = modulestore().get_item(location, depth=3) sections = course.get_children() return render_to_response('overview.html', { @@ -208,19 +213,14 @@ def course_index(request, org, course, name): @login_required def edit_subsection(request, location): # check that we have permissions to edit this item - if not has_access(request.user, location): + course = get_course_for_item(location) + if not has_access(request.user, course.location): raise PermissionDenied() - item = modulestore().get_item(location) + item = modulestore().get_item(location, depth=1) - # TODO: we need a smarter way to figure out what course an item is in - for course in modulestore().get_courses(): - if (course.location.org == item.location.org and - course.location.course == item.location.course): - break - - lms_link = get_lms_link_for_item(location) - preview_link = get_lms_link_for_item(location, preview=True) + lms_link = get_lms_link_for_item(location, course_id=course.location.course_id) + preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True) # make sure that location references a 'sequential', otherwise return BadRequest if item.location.category != 'sequential': @@ -241,8 +241,7 @@ def edit_subsection(request, location): (field.name, field.read_from(item)) for field in item.fields - if field.name not in ['display_name', 'start', 'due', 'format'] and - field.scope == Scope.settings + if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings ) can_view_live = False @@ -254,18 +253,18 @@ def edit_subsection(request, location): break return render_to_response('edit_subsection.html', - {'subsection': item, - 'context_course': course, - 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), - 'lms_link': lms_link, - 'preview_link': preview_link, - 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), - 'parent_location': course.location, - 'parent_item': parent, - 'policy_metadata': policy_metadata, - 'subsection_units': subsection_units, - 'can_view_live': can_view_live - }) + {'subsection': item, + 'context_course': course, + 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), + 'lms_link': lms_link, + 'preview_link': preview_link, + 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), + 'parent_location': course.location, + 'parent_item': parent, + 'policy_metadata': policy_metadata, + 'subsection_units': subsection_units, + 'can_view_live': can_view_live + }) @login_required @@ -277,19 +276,13 @@ def edit_unit(request, location): id: A Location URL """ - # check that we have permissions to edit this item - if not has_access(request.user, location): + course = get_course_for_item(location) + if not has_access(request.user, course.location): raise PermissionDenied() - item = modulestore().get_item(location) + item = modulestore().get_item(location, depth=1) - # TODO: we need a smarter way to figure out what course an item is in - for course in modulestore().get_courses(): - if (course.location.org == item.location.org and - course.location.course == item.location.course): - break - - lms_link = get_lms_link_for_item(item.location) + lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id) component_templates = defaultdict(list) @@ -350,17 +343,17 @@ def edit_unit(request, location): index = index + 1 preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', - 'preview.' + settings.LMS_BASE) + 'preview.' + settings.LMS_BASE) preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format( - preview_lms_base=preview_lms_base, - lms_base=settings.LMS_BASE, - org=course.location.org, - course=course.location.course, - course_name=course.location.name, - section=containing_section.location.name, - subsection=containing_subsection.location.name, - index=index) + preview_lms_base=preview_lms_base, + lms_base=settings.LMS_BASE, + org=course.location.org, + course=course.location.course, + course_name=course.location.name, + section=containing_section.location.name, + subsection=containing_subsection.location.name, + index=index) unit_state = compute_unit_state(item) @@ -374,7 +367,7 @@ def edit_unit(request, location): 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, 'subsection': containing_subsection, - 'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None, + 'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None, 'section': containing_section, 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'unit_state': unit_state, @@ -448,9 +441,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None): # Let the module handle the AJAX try: ajax_return = instance.handle_ajax(dispatch, request.POST) + except NotFoundError: log.exception("Module indicating to user that request doesn't exist") raise Http404 + + except ProcessingError: + log.warning("Module raised an error while processing AJAX request", + exc_info=True) + return HttpResponseBadRequest() + except: log.exception("error processing ajax call") raise @@ -615,26 +615,14 @@ def delete_item(request): delete_children = request.POST.get('delete_children', False) delete_all_versions = request.POST.get('delete_all_versions', False) - item = modulestore().get_item(item_location) + store = modulestore() - store = get_modulestore(item_loc) - - - # @TODO: this probably leaves draft items dangling. My preferance would be for the semantic to be - # if item.location.revision=None, then delete both draft and published version - # if caller wants to only delete the draft than the caller should put item.location.revision='draft' + item = store.get_item(item_location) if delete_children: - _xmodule_recurse(item, lambda i: store.delete_item(i.location)) + _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions)) else: - store.delete_item(item.location) - - # cdodge: this is a bit of a hack until I can talk with Cale about the - # semantics of delete_item whereby the store is draft aware. Right now calling - # delete_item on a vertical tries to delete the draft version leaving the - # requested delete to never occur - if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions: - modulestore('direct').delete_item(item.location) + store.delete_item(item.location, delete_all_versions) # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling if delete_all_versions: @@ -661,7 +649,7 @@ def save_item(request): if not has_access(request.user, item_location): raise PermissionDenied() - store = get_modulestore(Location(item_location)); + store = get_modulestore(Location(item_location)) if request.POST.get('data') is not None: data = request.POST['data'] @@ -796,7 +784,7 @@ def upload_asset(request, org, course, coursename): # Does the course actually exist?!? Get anything from it to prove its existance try: - item = modulestore().get_item(location) + modulestore().get_item(location) except: # no return it as a Bad Request response logging.error('Could not find course' + location) @@ -830,24 +818,23 @@ def upload_asset(request, org, course, coursename): readback = contentstore().find(content.location) response_payload = {'displayname': content.name, - 'uploadDate': get_date_display(readback.last_modified_at), - 'url': StaticContent.get_url_path_from_location(content.location), - 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None, - 'msg': 'Upload completed' - } + 'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()), + 'url': StaticContent.get_url_path_from_location(content.location), + 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None, + 'msg': 'Upload completed' + } response = HttpResponse(json.dumps(response_payload)) response['asset_url'] = StaticContent.get_url_path_from_location(content.location) return response -''' -This view will return all CMS users who are editors for the specified course -''' @login_required @ensure_csrf_cookie def manage_users(request, location): - + ''' + This view will return all CMS users who are editors for the specified course + ''' # check that logged in user has permissions to this item if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME): raise PermissionDenied() @@ -874,14 +861,14 @@ def create_json_response(errmsg=None): return resp -''' -This POST-back view will add a user - specified by email - to the list of editors for -the specified course -''' @expect_json @login_required @ensure_csrf_cookie def add_user(request, location): + ''' + This POST-back view will add a user - specified by email - to the list of editors for + the specified course + ''' email = request.POST["email"] if email == '': @@ -907,14 +894,15 @@ def add_user(request, location): return create_json_response() -''' -This POST-back view will remove a user - specified by email - from the list of editors for -the specified course -''' @expect_json @login_required @ensure_csrf_cookie def remove_user(request, location): + ''' + This POST-back view will remove a user - specified by email - from the list of editors for + the specified course + ''' + email = request.POST["email"] # check that logged in user has admin permissions on this course @@ -989,13 +977,12 @@ def reorder_static_tabs(request): for tab in course.tabs: if tab['type'] == 'static_tab': reordered_tabs.append({'type': 'static_tab', - 'name': tab_items[static_tab_idx].display_name, - 'url_slug': tab_items[static_tab_idx].location.name}) + 'name': tab_items[static_tab_idx].display_name, + 'url_slug': tab_items[static_tab_idx].location.name}) static_tab_idx += 1 else: reordered_tabs.append(tab) - # OK, re-assemble the static tabs in the new order course.tabs = reordered_tabs modulestore('direct').update_metadata(course.location, own_metadata(course)) @@ -1007,7 +994,6 @@ def reorder_static_tabs(request): def edit_tabs(request, org, course, coursename): location = ['i4x', org, course, 'course', coursename] course_item = modulestore().get_item(location) - static_tabs_loc = Location('i4x', org, course, 'static_tab', None) # check that logged in user has permissions to this item if not has_access(request.user, location): @@ -1036,7 +1022,7 @@ def edit_tabs(request, org, course, coursename): 'active_tab': 'pages', 'context_course': course_item, 'components': components - }) + }) def not_found(request): @@ -1098,21 +1084,21 @@ def course_info_updates(request, org, course, provided_id=None): if request.method == 'GET': return HttpResponse(json.dumps(get_course_updates(location)), - mimetype="application/json") + mimetype="application/json") elif real_method == 'DELETE': try: return HttpResponse(json.dumps(delete_course_update(location, - request.POST, provided_id)), mimetype="application/json") + request.POST, provided_id)), mimetype="application/json") except: return HttpResponseBadRequest("Failed to delete", - content_type="text/plain") + content_type="text/plain") elif request.method == 'POST': try: return HttpResponse(json.dumps(update_course_updates(location, - request.POST, provided_id)), mimetype="application/json") + request.POST, provided_id)), mimetype="application/json") except: return HttpResponseBadRequest("Failed to save", - content_type="text/plain") + content_type="text/plain") @expect_json @@ -1180,7 +1166,7 @@ def course_config_graders_page(request, org, course, name): return render_to_response('settings_graders.html', { 'context_course': course_module, - 'course_location' : location, + 'course_location': location, 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder) }) @@ -1199,8 +1185,8 @@ def course_config_advanced_page(request, org, course, name): return render_to_response('settings_advanced.html', { 'context_course': course_module, - 'course_location' : location, - 'advanced_dict' : json.dumps(CourseMetadata.fetch(location)), + 'course_location': location, + 'advanced_dict': json.dumps(CourseMetadata.fetch(location)), }) @@ -1221,7 +1207,8 @@ def course_settings_updates(request, org, course, name, section): manager = CourseDetails elif section == 'grading': manager = CourseGradingModel - else: return + else: + return if request.method == 'GET': # Cannot just do a get w/o knowing the course name :-( @@ -1273,14 +1260,48 @@ def course_advanced_updates(request, org, course, name): location = get_location_and_verify_access(request, org, course, name) real_method = get_request_method(request) - + if real_method == 'GET': return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json") elif real_method == 'DELETE': - return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json") + return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), + mimetype="application/json") elif real_method == 'POST' or real_method == 'PUT': # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key - return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json") + request_body = json.loads(request.body) + #Whether or not to filter the tabs key out of the settings metadata + filter_tabs = True + #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab + #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading + #module, and to remove it if they have removed the open ended elements. + if ADVANCED_COMPONENT_POLICY_KEY in request_body: + #Check to see if the user instantiated any open ended components + found_oe_type = False + #Get the course so that we can scrape current tabs + course_module = modulestore().get_item(location) + for oe_type in OPEN_ENDED_COMPONENT_TYPES: + if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]: + #Add an open ended tab to the course if needed + changed, new_tabs = add_open_ended_panel_tab(course_module) + #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json + if changed: + request_body.update({'tabs': new_tabs}) + #Indicate that tabs should not be filtered out of the metadata + filter_tabs = False + #Set this flag to avoid the open ended tab removal code below. + found_oe_type = True + break + #If we did not find an open ended module type in the advanced settings, + # we may need to remove the open ended tab from the course. + if not found_oe_type: + #Remove open ended tab to the course if needed + changed, new_tabs = remove_open_ended_panel_tab(course_module) + if changed: + request_body.update({'tabs': new_tabs}) + #Indicate that tabs should not be filtered out of the metadata + filter_tabs = False + response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs)) + return HttpResponse(response_json, mimetype="application/json") @ensure_csrf_cookie @@ -1308,10 +1329,10 @@ def get_checklists(request, org, course, name): if copied or modified: modulestore.update_metadata(location, own_metadata(course_module)) return render_to_response('checklists.html', - { - 'context_course': course_module, - 'checklists': checklists - }) + { + 'context_course': course_module, + 'checklists': checklists + }) @ensure_csrf_cookie @@ -1396,13 +1417,12 @@ def asset_index(request, org, course, name): # sort in reverse upload date order assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True) - thumbnails = contentstore().get_all_content_thumbnails_for_course(course_reference) asset_display = [] for asset in assets: id = asset['_id'] display_info = {} display_info['displayname'] = asset['displayname'] - display_info['uploadDate'] = get_date_display(asset['uploadDate']) + display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple()) asset_location = StaticContent.compute_location(id['org'], id['course'], id['name']) display_info['url'] = StaticContent.get_url_path_from_location(asset_location) @@ -1467,6 +1487,12 @@ def create_new_course(request): new_course = modulestore('direct').clone_item(template, dest_location) + # clone a default 'about' module as well + + about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview']) + dest_about_location = dest_location._replace(category='about', name='overview') + modulestore('direct').clone_item(about_template_location, dest_about_location) + if display_name is not None: new_course.display_name = display_name @@ -1490,10 +1516,10 @@ def initialize_course_tabs(course): # This logic is repeated in xmodule/modulestore/tests/factories.py # so if you change anything here, you need to also change it there. course.tabs = [{"type": "courseware"}, - {"type": "course_info", "name": "Course Info"}, - {"type": "discussion", "name": "Discussion"}, - {"type": "wiki", "name": "Wiki"}, - {"type": "progress", "name": "Progress"}] + {"type": "course_info", "name": "Course Info"}, + {"type": "discussion", "name": "Discussion"}, + {"type": "wiki", "name": "Wiki"}, + {"type": "progress", "name": "Progress"}] modulestore('direct').update_metadata(course.location.url(), own_metadata(course)) @@ -1549,7 +1575,10 @@ def import_course(request, org, course, name): shutil.move(r / fname, course_dir) module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT, - [course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location)) + [course_subdir], load_error_modules=False, + static_content_store=contentstore(), + target_location_namespace=Location(location), + draft_store=modulestore()) # we can blow this away when we're done importing. shutil.rmtree(course_dir) @@ -1583,8 +1612,8 @@ def generate_export_course(request, org, course, name): logging.debug('root = {0}'.format(root_dir)) - export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name) - # filename = root_dir / name + '.tar.gz' + export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore()) + #filename = root_dir / name + '.tar.gz' logging.debug('tar file being generated at {0}'.format(export_file.name)) tf = tarfile.open(name=export_file.name, mode='w:gz') diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index d3cd5fe164..0dbb47b31b 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -1,4 +1,3 @@ -from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.inheritance import own_metadata @@ -6,9 +5,9 @@ import json from json.encoder import JSONEncoder import time from contentstore.utils import get_modulestore -from util.converters import jsdate_to_time, time_to_date from models.settings import course_grading from contentstore.utils import update_item +from xmodule.fields import Date import re import logging @@ -81,8 +80,14 @@ class CourseDetails(object): dirty = False + # In the descriptor's setter, the date is converted to JSON using Date's to_json method. + # Calling to_json on something that is already JSON doesn't work. Since reaching directly + # into the model is nasty, convert the JSON Date to a Python date, which is what the + # setter expects as input. + date = Date() + if 'start_date' in jsondict: - converted = jsdate_to_time(jsondict['start_date']) + converted = date.from_json(jsondict['start_date']) else: converted = None if converted != descriptor.start: @@ -90,7 +95,7 @@ class CourseDetails(object): descriptor.start = converted if 'end_date' in jsondict: - converted = jsdate_to_time(jsondict['end_date']) + converted = date.from_json(jsondict['end_date']) else: converted = None @@ -99,7 +104,7 @@ class CourseDetails(object): descriptor.end = converted if 'enrollment_start' in jsondict: - converted = jsdate_to_time(jsondict['enrollment_start']) + converted = date.from_json(jsondict['enrollment_start']) else: converted = None @@ -108,7 +113,7 @@ class CourseDetails(object): descriptor.enrollment_start = converted if 'enrollment_end' in jsondict: - converted = jsdate_to_time(jsondict['enrollment_end']) + converted = date.from_json(jsondict['enrollment_end']) else: converted = None @@ -169,7 +174,6 @@ class CourseDetails(object): return result - # TODO move to a more general util? Is there a better way to do the isinstance model check? class CourseSettingsEncoder(json.JSONEncoder): def default(self, obj): @@ -178,6 +182,6 @@ class CourseSettingsEncoder(json.JSONEncoder): elif isinstance(obj, Location): return obj.dict() elif isinstance(obj, time.struct_time): - return time_to_date(obj) + return Date().to_json(obj) else: return JSONEncoder.default(self, obj) diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index b20fb71f66..4ea9f2f5db 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -1,7 +1,5 @@ from xmodule.modulestore import Location from contentstore.utils import get_modulestore -import re -from util import converters from datetime import timedelta @@ -47,14 +45,13 @@ class CourseGradingModel(object): # return empty model else: - return { - "id": index, + return {"id": index, "type": "", "min_count": 0, "drop_count": 0, "short_label": None, "weight": 0 - } + } @staticmethod def fetch_cutoffs(course_location): @@ -97,7 +94,6 @@ class CourseGradingModel(object): return CourseGradingModel.fetch(course_location) - @staticmethod def update_grader_from_json(course_location, grader): """ @@ -139,7 +135,6 @@ class CourseGradingModel(object): return cutoffs - @staticmethod def update_grace_period_from_json(course_location, graceperiodjson): """ @@ -212,8 +207,7 @@ class CourseGradingModel(object): location = Location(location) descriptor = get_modulestore(location).get_item(location) - return { - "graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded', + return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded', "location": location, "id": 99 # just an arbitrary value to } @@ -233,7 +227,6 @@ class CourseGradingModel(object): get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata) - @staticmethod def convert_set_grace_period(descriptor): # 5 hours 59 minutes 59 seconds => converted to iso format @@ -264,13 +257,12 @@ class CourseGradingModel(object): @staticmethod def parse_grader(json_grader): # manual to clear out kruft - result = { - "type": json_grader["type"], - "min_count": int(json_grader.get('min_count', 0)), - "drop_count": int(json_grader.get('drop_count', 0)), - "short_label": json_grader.get('short_label', None), - "weight": float(json_grader.get('weight', 0)) / 100.0 - } + result = {"type": json_grader["type"], + "min_count": int(json_grader.get('min_count', 0)), + "drop_count": int(json_grader.get('drop_count', 0)), + "short_label": json_grader.get('short_label', None), + "weight": float(json_grader.get('weight', 0)) / 100.0 + } return result diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 563dd16524..4429e35692 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -4,6 +4,7 @@ from xmodule.x_module import XModuleDescriptor from xmodule.modulestore.inheritance import own_metadata from xblock.core import Scope from xmodule.course_module import CourseDescriptor +import copy class CourseMetadata(object): @@ -13,8 +14,13 @@ class CourseMetadata(object): The objects have no predefined attrs but instead are obj encodings of the editable metadata. ''' - FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', - 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists'] + FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', + 'end', + 'enrollment_start', + 'enrollment_end', + 'tabs', + 'graceperiod', + 'checklists'] @classmethod def fetch(cls, course_location): @@ -39,7 +45,7 @@ class CourseMetadata(object): return course @classmethod - def update_from_json(cls, course_location, jsondict): + def update_from_json(cls, course_location, jsondict, filter_tabs=True): """ Decode the json into CourseMetadata and save any changed attrs to the db. @@ -49,9 +55,15 @@ class CourseMetadata(object): dirty = False + #Copy the filtered list to avoid permanently changing the class attribute + filtered_list = copy.copy(cls.FILTERED_LIST) + #Don't filter on the tab attribute if filter_tabs is False + if not filter_tabs: + filtered_list.remove("tabs") + for k, v in jsondict.iteritems(): # should it be an error if one of the filtered list items is in the payload? - if k in cls.FILTERED_LIST: + if k in filtered_list: continue if hasattr(descriptor, k) and getattr(descriptor, k) != v: @@ -65,7 +77,7 @@ class CourseMetadata(object): if dirty: get_modulestore(course_location).update_metadata(course_location, - own_metadata(descriptor)) + own_metadata(descriptor)) # Could just generate and return a course obj w/o doing any db reads, # but I put the reads in as a means to confirm it persisted correctly @@ -86,6 +98,6 @@ class CourseMetadata(object): delattr(descriptor.lms, key) get_modulestore(course_location).update_metadata(course_location, - own_metadata(descriptor)) + own_metadata(descriptor)) return cls.fetch(course_location) diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 26a8adc92c..1e7a32dc68 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -36,3 +36,4 @@ DATABASES = { INSTALLED_APPS += ('lettuce.django',) LETTUCE_APPS = ('contentstore',) LETTUCE_SERVER_PORT = 8001 +LETTUCE_BROWSER = 'chrome' diff --git a/cms/envs/aws.py b/cms/envs/aws.py index be7816d21f..59ad8b835e 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -46,6 +46,9 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items(): MITX_FEATURES[feature] = value +# load segment.io key, provide a dummy if it does not exist +SEGMENT_IO_KEY = ENV_TOKENS.get('SEGMENT_IO_KEY', '***REMOVED***') + LOGGING = get_logger_config(LOG_DIR, logging_env=ENV_TOKENS['LOGGING_ENV'], syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514), @@ -64,4 +67,4 @@ MODULESTORE = AUTH_TOKENS['MODULESTORE'] CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE'] # Datadog for events! -DATADOG_API = AUTH_TOKENS.get("DATADOG_API") \ No newline at end of file +DATADOG_API = AUTH_TOKENS.get("DATADOG_API") diff --git a/cms/envs/common.py b/cms/envs/common.py index a83f61d8f9..8effc773e0 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -20,11 +20,8 @@ Longer TODO: """ import sys -import os.path -import os import lms.envs.common from path import path -from xmodule.static_content import write_descriptor_styles, write_descriptor_js, write_module_js, write_module_styles ############################ FEATURE CONFIGURATION ############################# @@ -34,6 +31,9 @@ MITX_FEATURES = { 'ENABLE_DISCUSSION_SERVICE': False, 'AUTH_USE_MIT_CERTIFICATES': False, 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests + 'STAFF_EMAIL': '', # email address for staff (eg to request course creation) + 'STUDIO_NPS_SURVEY': True, + 'SEGMENT_IO': True, } ENABLE_JASMINE = False @@ -113,6 +113,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'contentserver.middleware.StaticContentServer', + 'request_cache.middleware.RequestCache', 'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -125,6 +126,9 @@ MIDDLEWARE_CLASSES = ( 'track.middleware.TrackMiddleware', 'mitxmako.middleware.MakoMiddleware', + # Detects user-requested locale from 'accept-language' header in http request + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.transaction.TransactionMiddleware' ) @@ -163,15 +167,19 @@ STATICFILES_DIRS = [ PROJECT_ROOT / "static", # This is how you would use the textbook images locally -# ("book", ENV_ROOT / "book_images") +# ("book", ENV_ROOT / "book_images") ] # Locale/Internationalization TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html + USE_I18N = True USE_L10N = True +# Localization strings (e.g. django.po) are under this directory +LOCALE_PATHS = (REPO_ROOT + '/conf/locale',) # mitx/conf/locale/ + # Tracking TRACK_MAX_EVENT = 10000 @@ -182,29 +190,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' -# Load javascript and css from all of the available descriptors, and -# prep it for use in pipeline js -from xmodule.raw_module import RawDescriptor -from xmodule.error_module import ErrorDescriptor -from rooted_paths import rooted_glob, remove_root - -write_descriptor_styles(PROJECT_ROOT / "static/sass/descriptor", [RawDescriptor, ErrorDescriptor]) -write_module_styles(PROJECT_ROOT / "static/sass/module", [RawDescriptor, ErrorDescriptor]) - -descriptor_js = remove_root( - PROJECT_ROOT / 'static', - write_descriptor_js( - PROJECT_ROOT / "static/coffee/descriptor", - [RawDescriptor, ErrorDescriptor] - ) -) -module_js = remove_root( - PROJECT_ROOT / 'static', - write_module_js( - PROJECT_ROOT / "static/coffee/module", - [RawDescriptor, ErrorDescriptor] - ) -) +from rooted_paths import rooted_glob PIPELINE_CSS = { 'base-style': { @@ -212,39 +198,35 @@ PIPELINE_CSS = { 'js/vendor/CodeMirror/codemirror.css', 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/jquery.qtip.min.css', - 'sass/base-style.scss' + 'sass/base-style.css', + 'xmodule/modules.css', + 'xmodule/descriptor.css', ], 'output_filename': 'css/cms-base-style.css', }, } -PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] - +# test_order: Determines the position of this chunk of javascript on +# the jasmine test page PIPELINE_JS = { 'main': { 'source_filenames': sorted( - rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') + - rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee') + rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') + + rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js') ) + ['js/hesitate.js', 'js/base.js'], 'output_filename': 'js/cms-application.js', + 'test_order': 0 }, 'module-js': { - 'source_filenames': descriptor_js + module_js, + 'source_filenames': ( + rooted_glob(COMMON_ROOT / 'static/', 'xmodule/descriptors/js/*.js') + + rooted_glob(COMMON_ROOT / 'static/', 'xmodule/modules/js/*.js') + ), 'output_filename': 'js/cms-modules.js', + 'test_order': 1 }, - 'spec': { - 'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.coffee')), - 'output_filename': 'js/cms-spec.js' - } } -PIPELINE_COMPILERS = [ - 'pipeline.compilers.sass.SASSCompiler', - 'pipeline.compilers.coffee.CoffeeScriptCompiler', -] - -PIPELINE_SASS_ARGUMENTS = '-t compressed -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) - PIPELINE_CSS_COMPRESSOR = None PIPELINE_JS_COMPRESSOR = None @@ -256,11 +238,6 @@ STATICFILES_IGNORE_PATTERNS = ( ) PIPELINE_YUI_BINARY = 'yui-compressor' -PIPELINE_SASS_BINARY = 'sass' -PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee' - -# Setting that will only affect the MITx version of django-pipeline until our changes are merged upstream -PIPELINE_COMPILE_INPLACE = True ############################ APPS ##################################### diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 5612db1396..dbf9c5574c 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0 # Dummy secret key for dev SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' +################################ PIPELINE ################################# + +PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) + ################################ DEBUG TOOLBAR ################################# INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo') MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware', @@ -142,4 +146,10 @@ DEBUG_TOOLBAR_CONFIG = { # To see stacktraces for MongoDB queries, set this to True. # Stacktraces slow down page loads drastically (for pages with lots of queries). -DEBUG_TOOLBAR_MONGO_STACKTRACES = False +DEBUG_TOOLBAR_MONGO_STACKTRACES = True + +# disable NPS survey in dev mode +MITX_FEATURES['STUDIO_NPS_SURVEY'] = False + +# segment-io key for dev +SEGMENT_IO_KEY = 'mty8edrrsg' diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py index 5c9be1cf9c..e046a6d37c 100644 --- a/cms/envs/jasmine.py +++ b/cms/envs/jasmine.py @@ -20,14 +20,14 @@ PIPELINE_JS['js-test-source'] = { 'source_filenames': sum([ pipeline_group['source_filenames'] for group_name, pipeline_group - in PIPELINE_JS.items() + in sorted(PIPELINE_JS.items(), key=lambda item: item[1].get('test_order', 1e100)) if group_name != 'spec' ], []), 'output_filename': 'js/cms-test-source.js' } PIPELINE_JS['spec'] = { - 'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.coffee')), + 'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.js')), 'output_filename': 'js/cms-spec.js' } @@ -35,4 +35,10 @@ JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib') +# Remove the localization middleware class because it requires the test database +# to be sync'd and migrated in order to run the jasmine tests interactively +# with a browser +MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \ + if e != 'django.middleware.locale.LocaleMiddleware') + INSTALLED_APPS += ('django_jasmine', ) diff --git a/cms/envs/test.py b/cms/envs/test.py index d7992cb471..63b5efc645 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -13,14 +13,10 @@ from path import path # Nose Test Runner INSTALLED_APPS += ('django_nose',) -NOSE_ARGS = ['--with-xunit'] TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_ROOT = path('test_root') -# Makes the tests run much faster... -SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead - # Want static files in the same dir for running on jenkins. STATIC_ROOT = TEST_ROOT / "staticfiles" @@ -28,7 +24,7 @@ GITHUB_REPO_ROOT = TEST_ROOT / "data" COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" # Makes the tests run much faster... -SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead +SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead # TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing STATICFILES_DIRS = [ @@ -41,7 +37,7 @@ STATICFILES_DIRS += [ if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir) ] -modulestore_options = { +MODULESTORE_OPTIONS = { 'default_class': 'xmodule.raw_module.RawDescriptor', 'host': 'localhost', 'db': 'test_xmodule', @@ -53,11 +49,15 @@ modulestore_options = { MODULESTORE = { 'default': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': modulestore_options + 'OPTIONS': MODULESTORE_OPTIONS }, 'direct': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': modulestore_options + 'OPTIONS': MODULESTORE_OPTIONS + }, + 'draft': { + 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', + 'OPTIONS': MODULESTORE_OPTIONS } } @@ -72,7 +72,7 @@ CONTENTSTORE = { DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ENV_ROOT / "db" / "cms.db", + 'NAME': TEST_ROOT / "db" / "cms.db", }, } @@ -114,3 +114,10 @@ PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher', ) + +# dummy segment-io key +SEGMENT_IO_KEY = '***REMOVED***' + +# disable NPS survey in test mode +MITX_FEATURES['STUDIO_NPS_SURVEY'] = False + diff --git a/cms/one_time_startup.py b/cms/one_time_startup.py index 38a2fef847..5cc74a8fd7 100644 --- a/cms/one_time_startup.py +++ b/cms/one_time_startup.py @@ -1,14 +1,19 @@ from dogapi import dog_http_api, dog_stats_api from django.conf import settings from xmodule.modulestore.django import modulestore +from django.dispatch import Signal +from request_cache.middleware import RequestCache -from django.core.cache import get_cache, InvalidCacheBackendError +from django.core.cache import get_cache cache = get_cache('mongo_metadata_inheritance') for store_name in settings.MODULESTORE: store = modulestore(store_name) - store.metadata_inheritance_cache = cache + store.metadata_inheritance_cache_subsystem = cache + store.request_cache = RequestCache.get_request_cache() + modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location']) + store.modulestore_update_signal = modulestore_update_signal if hasattr(settings, 'DATADOG_API'): dog_http_api.api_key = settings.DATADOG_API dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True) diff --git a/cms/static/client_templates/checklist.html b/cms/static/client_templates/checklist.html index ec6ff4e892..6884b0e9c9 100644 --- a/cms/static/client_templates/checklist.html +++ b/cms/static/client_templates/checklist.html @@ -44,7 +44,7 @@ <% if (item['action_text'] !== '' && item['action_url'] !== '') { %>
    -
  • +
  • rel="external" title="This link will open in a new browser window/tab" diff --git a/cms/static/coffee/.gitignore b/cms/static/coffee/.gitignore index e114474f98..a6c7c2852d 100644 --- a/cms/static/coffee/.gitignore +++ b/cms/static/coffee/.gitignore @@ -1,3 +1 @@ *.js -descriptor -module diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json index e7a66b5bc0..c2e1a8acf6 100644 --- a/cms/static/coffee/files.json +++ b/cms/static/coffee/files.json @@ -7,6 +7,7 @@ "js/vendor/jquery.cookie.js", "js/vendor/json2.js", "js/vendor/underscore-min.js", - "js/vendor/backbone-min.js" + "js/vendor/backbone-min.js", + "js/vendor/jquery.leanModal.min.js" ] } diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee index 9f7e3a5e60..3cb3b1703f 100644 --- a/cms/static/coffee/src/views/module_edit.coffee +++ b/cms/static/coffee/src/views/module_edit.coffee @@ -15,7 +15,7 @@ class CMS.Views.ModuleEdit extends Backbone.View $component_editor: => @$el.find('.component-editor') loadDisplay: -> - XModule.loadModule(@$el.find('.xmodule_display')) + XModule.loadModule(@$el.find('.xmodule_display')) loadEdit: -> if not @module @@ -55,6 +55,11 @@ class CMS.Views.ModuleEdit extends Backbone.View clickSaveButton: (event) => event.preventDefault() data = @module.save() + + analytics.track "Saved Module", + course: course_location_analytics + id: _this.model.id + data.metadata = _.extend(data.metadata || {}, @metadata()) @hideModal() @model.save(data).done( => diff --git a/cms/static/coffee/src/views/tabs.coffee b/cms/static/coffee/src/views/tabs.coffee index 9fbe4e5789..1034fc988e 100644 --- a/cms/static/coffee/src/views/tabs.coffee +++ b/cms/static/coffee/src/views/tabs.coffee @@ -28,6 +28,10 @@ class CMS.Views.TabsEdit extends Backbone.View @$('.component').each((idx, element) => tabs.push($(element).data('id')) ) + + analytics.track "Reordered Static Pages", + course: course_location_analytics + $.ajax({ type:'POST', url: '/reorder_static_tabs', @@ -56,10 +60,18 @@ class CMS.Views.TabsEdit extends Backbone.View 'i4x://edx/templates/static_tab/Empty' ) + analytics.track "Added Static Page", + course: course_location_analytics + deleteTab: (event) => if not confirm 'Are you sure you want to delete this component? This action cannot be undone.' return $component = $(event.currentTarget).parents('.component') + + analytics.track "Deleted Static Page", + course: course_location_analytics + id: $component.data('id') + $.post('/delete_item', { id: $component.data('id') }, => diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee index 42127b2800..e23477ccfa 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -35,6 +35,10 @@ class CMS.Views.UnitEdit extends Backbone.View @$('.components').sortable( handle: '.drag-handle' update: (event, ui) => + analytics.track "Reordered Components", + course: course_location_analytics + id: unit_location_analytics + payload = children : @components() options = success : => @model.unset('children') @model.save(payload, options) @@ -89,6 +93,11 @@ class CMS.Views.UnitEdit extends Backbone.View $(event.currentTarget).data('location') ) + analytics.track "Added a Component", + course: course_location_analytics + unit_id: unit_location_analytics + type: $(event.currentTarget).data('location') + @closeNewComponent(event) components: => @$('.component').map((idx, el) -> $(el).data('id')).get() @@ -111,6 +120,11 @@ class CMS.Views.UnitEdit extends Backbone.View $.post('/delete_item', { id: $component.data('id') }, => + analytics.track "Deleted a Component", + course: course_location_analytics + unit_id: unit_location_analytics + id: $component.data('id') + $component.remove() # b/c we don't vigilantly keep children up to date # get rid of it before it hurts someone @@ -129,6 +143,10 @@ class CMS.Views.UnitEdit extends Backbone.View id: @$el.data('id') delete_children: true }, => + analytics.track "Deleted Draft", + course: course_location_analytics + unit_id: unit_location_analytics + window.location.reload() ) @@ -138,6 +156,10 @@ class CMS.Views.UnitEdit extends Backbone.View $.post('/create_draft', { id: @$el.data('id') }, => + analytics.track "Created Draft", + course: course_location_analytics + unit_id: unit_location_analytics + @model.set('state', 'draft') ) @@ -148,20 +170,31 @@ class CMS.Views.UnitEdit extends Backbone.View $.post('/publish_draft', { id: @$el.data('id') }, => + analytics.track "Published Draft", + course: course_location_analytics + unit_id: unit_location_analytics + @model.set('state', 'public') ) setVisibility: (event) -> if @$('.visibility-select').val() == 'private' target_url = '/unpublish_unit' + visibility = "private" else target_url = '/publish_draft' + visibility = "public" @wait(true) $.post(target_url, { id: @$el.data('id') }, => + analytics.track "Set Unit Visibility", + course: course_location_analytics + unit_id: unit_location_analytics + visibility: visibility + @model.set('state', @$('.visibility-select').val()) ) @@ -193,6 +226,11 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View @model.save(metadata: metadata) # Update name shown in the right-hand side location summary. $('.unit-location .editing .unit-name').html(metadata.display_name) + analytics.track "Edited Unit Name", + course: course_location_analytics + unit_id: unit_location_analytics + display_name: metadata.display_name + class CMS.Views.UnitEdit.LocationState extends Backbone.View initialize: => diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 0521371b6a..ad81963b0f 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -1,11 +1,16 @@ +if (!window.CmsUtils) window.CmsUtils = {}; + var $body; var $modal; var $modalCover; var $newComponentItem; var $changedInput; var $spinner; +var $newComponentTypePicker; +var $newComponentTemplatePickers; +var $newComponentButton; -$(document).ready(function () { +$(document).ready(function() { $body = $('body'); $modal = $('.history-modal'); $modalCover = $(' - \ No newline at end of file diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index eb5a9a9824..901e0a8008 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -1,9 +1,7 @@ <%inherit file="base.html" /> <%! - from time import mktime - import dateutil.parser import logging - from datetime import datetime + from xmodule.util.date_utils import get_time_struct_display %> <%! from django.core.urlresolvers import reverse %> @@ -13,7 +11,6 @@ <%namespace name="units" file="widgets/units.html" /> <%namespace name='static' file='static_content.html'/> -<%namespace name='datetime' module='datetime'/> <%block name="content">
    @@ -36,20 +33,22 @@

    Subsection Settings

    -
    - <% - start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None - parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None - %> - - +
    + + +
    +
    + + +
    % if subsection.lms.start != parent_item.lms.start and subsection.lms.start: - % if parent_start_date is None: + % 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} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}. +

    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 %H:%M UTC')}. % endif Sync to ${parent_item.display_name_with_default}.

    % endif @@ -62,18 +61,17 @@
    - Set a due date
    -

    - <% - # due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use - due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None - %> - - - Remove due date -

    +
    + + +
    +
    + + +
    + Remove due date
    diff --git a/cms/templates/howitworks.html b/cms/templates/howitworks.html index 1cf9b17710..6c0029c425 100644 --- a/cms/templates/howitworks.html +++ b/cms/templates/howitworks.html @@ -134,10 +134,10 @@ @@ -151,7 +151,7 @@
    Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.
    - + close modal @@ -164,7 +164,7 @@
    Quickly create videos, text snippets, inline discussions, and a variety of problem types.
    - + close modal @@ -177,7 +177,7 @@
    Simply set the date of a section or subsection, and Studio will publish it to your students for you.
    - + close modal diff --git a/cms/templates/index.html b/cms/templates/index.html index 9482b9d9af..0f6e982b1d 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -1,6 +1,8 @@ +<%! from django.utils.translation import ugettext as _ %> + <%inherit file="base.html" /> -<%block name="title">My Courses +<%block name="title">${_("My Courses")} <%block name="bodyclass">is-signedin index dashboard <%block name="header_extras"> @@ -36,16 +38,18 @@
    -

    My Courses

    +

    ${_("My Courses")}

    % if user.is_active:
    - \ No newline at end of file + diff --git a/cms/templates/overview.html b/cms/templates/overview.html index 904f654717..e0fa2aa5b6 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -1,9 +1,7 @@ <%inherit file="base.html" /> <%! - from time import mktime - import dateutil.parser import logging - from datetime import datetime + from xmodule.util.date_utils import get_time_struct_display %> <%! from django.core.urlresolvers import reverse %> <%block name="title">Course Outline @@ -28,9 +26,9 @@ // I believe that current (New Section/New Subsection) cause full page reloads which means these aren't needed globally // but we really should change that behavior. if (!window.graderTypes) { - window.graderTypes = new CMS.Models.Settings.CourseGraderCollection(); - window.graderTypes.course_location = new CMS.Models.Location('${parent_location}'); - window.graderTypes.reset(${course_graders|n}); + window.graderTypes = new CMS.Models.Settings.CourseGraderCollection(); + window.graderTypes.course_location = new CMS.Models.Location('${parent_location}'); + window.graderTypes.reset(${course_graders|n}); } $(".gradable-status").each(function(index, ele) { @@ -106,20 +104,6 @@ <%block name="content"> - -
    @@ -163,15 +147,14 @@ @@ -200,7 +183,7 @@
    -
    +
    @@ -219,4 +202,25 @@
    + +
    +
    +

    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.

    +
    +
    + SaveCancel +
    +
    diff --git a/cms/templates/settings.html b/cms/templates/settings.html index e4cb4b3743..0a647c632e 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -4,7 +4,7 @@ <%namespace name='static' file='static_content.html'/> <%! -from contentstore import utils +from contentstore import utils %> @@ -13,17 +13,17 @@ from contentstore import utils - + - + @@ -62,10 +62,10 @@ from contentstore import utils
    -
    +

    Basic Information

    The nuts and bolts of your course -
    +
    1. @@ -83,45 +83,57 @@ from contentstore import utils
    - These are used in your course URL, and cannot be changed + +
    +

    Course Summary Page (for student enrollment and access)

    + + + +
    -
    +
    -
    +

    Course Schedule

    Important steps and segments of your course -
    +
    1. - First day the course begins -
      + First day the course begins +
    - -
    + +
  • - Last day your course is active -
    + Last day your course is active +
    - -
    -
  • + + +
      @@ -129,33 +141,33 @@ from contentstore import utils
      - First day students can enroll -
      + First day students can enroll +
      - -
      + +
    1. - Last day students can enroll -
      + Last day students can enroll +
      - -
      -
    2. + + +
    -
    +
    @@ -167,45 +179,44 @@ from contentstore import utils
  • - Introductions, prerequisites, FAQs that are used on your course summary page + Introductions, prerequisites, FAQs that are used on your course summary page (formatted in HTML)
  • -
    - +
    -
    +
    Enter your YouTube video's ID (along with any restriction parameters) -
    +
  • -
    +
    -
    +

    Requirements

    Expectations of the students taking this course -
    +
    1. - Time spent on all course work -
    2. -
    -
    + Time spent on all course work + + +
@@ -215,7 +226,7 @@ from contentstore import utils

Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.

Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.

- +
% if context_course: @@ -234,4 +245,4 @@ from contentstore import utils
- \ No newline at end of file + diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index 838af5ada9..c40427c9ec 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -19,6 +19,12 @@ from contentstore import utils - + - + @@ -97,7 +97,7 @@ from contentstore import utils
  1. - + Leeway on due dates
@@ -112,13 +112,13 @@ from contentstore import utils
    - -
+ + diff --git a/cms/templates/unit.html b/cms/templates/unit.html index e1a020dfca..cb34f42a09 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -16,7 +16,6 @@ }); $(document).ready(function() { - $('body').addClass('js'); // tabs $('.tab-group').tabs(); @@ -28,6 +27,7 @@ }); }); + var unit_location_analytics = '${unit_location}'; diff --git a/cms/templates/ux-alerts.html b/cms/templates/ux-alerts.html new file mode 100644 index 0000000000..de062e471e --- /dev/null +++ b/cms/templates/ux-alerts.html @@ -0,0 +1,544 @@ +<%inherit file="base.html" /> +<%block name="title">Studio Alerts +<%block name="bodyclass">is-signedin course uxdesign alerts + +<%block name="jsextra"> + + + +<%block name="content"> +
+
+
+ UX Design +

System Notifications

+
+
+
+ +
+
+
+
+
+

Alerts

+ persistant, static messages to the user +
+ +

In Studio, alerts are 1) general warnings/notes (e.g. drafts, published content, next steps) about the current view a user is interacting with or 2) notes about the status (e.g. saved confirmations, errors, next system steps) of any previous state that need to communicated to the user when arriving at the current view.

+ +

Different Static Examples of Alerts

+

Note: alerts will probably never been shown based on click or page action and will primarily be loaded along with a pageload and initial display

+ + +
+ +
+
+

Notifications

+ contextual, feedback-based, and temporal messages to the user +
+ +

In Studio, notifications are meant to inform the user of 1) any system status (e.g. saving, processing/validating) occurring based on any action they have taken or 2) any decisions (e.g. saving/discarding) a user must make to confirm.

+ +

Different Static Examples of Notifications

+ + +
+ +
+
+

Prompts

+ presents a user with a choice, based on their previous interaction, that must be decided before they can proceed +
+ +

In Studio, prompts are dialogs that are presented above all other page components and present a user with a choice, based on their previous interaction, that must be decided before they can proceed (or return to the previous interaction step).

+ +

Different Static Examples of Prompts

+ + +
+
+
+
+ + +<%block name="view_alerts"> + +
+
+ + +
+

You are editing a draft

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + +
+
+ + +
+
+ + +
+

A Newer Version of This Exists

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + +
+
+ + +
+
+ + +
+

Your changes have been saved

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

+
+
+
+ + +
+
+ + +
+

Your changes have been saved

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

+
+ + + + close alert + +
+
+ + +
+
+ + +
+

X Has been removed

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+
+
+ + +
+
+ + +
+

We're sorry, there was a error with Studio

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+
+
+ + +
+
+ + +
+

There was an error in your submission

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Please see below

+
+ + +
+
+ + +
+
+ 📢 + +
+

Studio will be unavailable this weekend

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+ + +
+
+ + +
+
+ 📢 + +
+

Studio will be unavailable this weekend

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.

+
+
+
+ + +
+
+ + +
+

Your Studio account has been created, but needs to be activated

+

Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. Cras mattis consectetur purus sit amet fermentum. Curabitur blandit tempus porttitor.

+
+ + +
+
+ + +<%block name="view_notifications"> + +
+
+ 📝 + +
+

You've Made Some Changes

+

Your changes will not take effect until you save your progress.

+
+ + +
+
+ + + + + + + + +
+
+ + +
+

Saving …

+
+
+
+ + +
+
+ + +
+

Your Section Has Been Created

+
+
+
+ + +
+
+ + +
+

Fun Fact:

+

Using the checkmark will allow you make a subsection gradable as an assignment, which counts towards a student's total grade

+
+ + + + close notification + +
+
+ + +<%block name="view_prompts"> + + + + + + + + + diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index 0f265dfc2c..db7d5fb3f8 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -1,9 +1,10 @@ <%! from django.core.urlresolvers import reverse %> +<%! from django.utils.translation import ugettext as _ %> - + diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index d601b940f5..167d5417d7 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -1,6 +1,6 @@ <%! from django.core.urlresolvers import reverse %> -
+
@@ -82,6 +82,19 @@ - + +
+

Are you sure that you want to flag this submission?

+

+ You are about to flag a submission. You should only flag a submission that contains explicit or offensive content. If the submission is not addressed to the question or is incorrect, you should give it a score of zero and accompanying feedback instead of flagging it. +

+
+ + +
+
+ + +
diff --git a/lms/templates/press.json b/lms/templates/press.json index b165037544..43c295b63e 100644 --- a/lms/templates/press.json +++ b/lms/templates/press.json @@ -1,428 +1,106 @@ [ - { - "title": "The Year of the MOOC", - "url": "http://www.nytimes.com/2012/11/04/education/edlife/massive-open-online-courses-are-multiplying-at-a-rapid-pace.html", - "author": "Laura Pappano", + "title": "Adapting to Blended Courses, and Finding Early Benefits", + "url": "http://www.nytimes.com/2013/04/30/education/adapting-to-blended-courses-and-finding-early-benefits.html?ref=education", + "author": "Tamar Lewin", "image": "nyt_logo_178x138.jpeg", "deck": null, "publication": "The New York Times", - "publish_date": "November 2, 2012" + "publish_date": "April 29, 2013" }, + { - "title": "The Most Important Education Technology in 200 Years", - "url": "http://www.technologyreview.com/news/506351/the-most-important-education-technology-in-200-years/", - "author": "Antonio Regalado", - "image": "techreview_logo_178x138.jpg", + "title": "Colleges Adapt Online Courses to Ease Burden", + "url": "http://www.nytimes.com/2013/04/30/education/colleges-adapt-online-courses-to-ease-burden.html?pagewanted=all", + "author": "Tamar Lewin", + "image": "nyt_logo_178x138.jpeg", "deck": null, - "publication": "Technology Review", - "publish_date": "November 2, 2012" + "publication": "The New York Times", + "publish_date": "April 29, 2013" }, + { - "title": "Classroom in the Cloud", - "url": "http://harvardmagazine.com/2012/11/classroom-in-the-cloud", - "author": null, - "image": "harvardmagazine_logo_178x138.jpeg", - "deck": null, - "publication": "Harvard Magazine", - "publish_date": "November-December 2012" - }, - { - "title": "How do you stop online students cheating?", - "url": "http://www.bbc.co.uk/news/business-19661899", - "author": "Sean Coughlan", - "image": "bbc_logo_178x138.jpeg", - "deck": null, - "publication": "BBC", - "publish_date": "October 31, 2012" - }, - { - "title": "VMware to provide software for HarvardX CS50x", - "url": "http://tech.mit.edu/V132/N48/edxvmware.html", - "author": "Stan Gill", - "image": "thetech_logo_178x138.jpg", - "deck": null, - "publication": "The Tech", - "publish_date": "October 26, 2012" - }, - { - "title": "EdX platform integrates into classes", - "url": "http://tech.mit.edu/V132/N48/801edx.html", - "author": "Leon Lin", - "image": "thetech_logo_178x138.jpg", - "deck": null, - "publication": "The Tech", - "publish_date": "October 26, 2012" - }, - { - "title": "VMware Offers Free Software to edX Learners", - "url": "http://campustechnology.com/articles/2012/10/25/vmware-offers-free-virtualization-software-for-edx-computer-science-students.aspx", - "author": "Joshua Bolkan", - "image": "campustech_logo_178x138.jpg", - "deck": "VMware Offers Free Virtualization Software for EdX Computer Science Students", - "publication": "Campus Technology", - "publish_date": "October 25, 2012" - }, - { - "title": "Lone Star moots charges to make Moocs add up", - "url": "http://www.timeshighereducation.co.uk/story.asp?sectioncode=26&storycode=421577&c=1", - "author": "David Matthews", - "image": "timeshighered_logo_178x138.jpg", - "deck": null, - "publication": "Times Higher Education", - "publish_date": "October 25, 2012" - }, - { - "title": "Free, high-quality and with mass appeal", - "url": "http://www.ft.com/intl/cms/s/2/73030f44-d4dd-11e1-9444-00144feabdc0.html#axzz2A9qvk48A", - "author": "Rebecca Knight", - "image": "ft_logo_178x138.jpg", - "deck": null, - "publication": "Financial Times", - "publish_date": "October 22, 2012" - }, - { - "title": "Getting the most out of an online education", - "url": "http://www.reuters.com/article/2012/10/19/us-education-courses-online-idUSBRE89I17120121019", - "author": "Kathleen Kingsbury", - "image": "reuters_logo_178x138.jpg", - "deck": null, - "publication": "Reuters", - "publish_date": "October 19, 2012" - }, - { - "title": "EdX announces partnership with Cengage", - "url": "http://tech.mit.edu/V132/N46/cengage.html", - "author": "Leon Lin", - "image": "thetech_logo_178x138.jpg", - "deck": null, - "publication": "The Tech", - "publish_date": "October 19, 2012" - }, - { - "title": "U Texas System Joins EdX", - "url": "http://campustechnology.com/articles/2012/10/18/u-texas-system-joins-edx.aspx", - "author": "Joshua Bolkan", - "image": "campustech_logo_178x138.jpg", - "deck": null, - "publication": "Campus Technology", - "publish_date": "October 18, 2012" - }, - { - "title": "San Jose State University Runs Blended Learning Course Using edX ", - "url": "http://chronicle.com/blogs/wiredcampus/san-jose-state-u-says-replacing-live-lectures-with-videos-increased-test-scores/40470", - "author": "Alisha Azevedo", - "image": "chroniclehighered_logo_178x138.jpeg", - "deck": "San Jose State U. Says Replacing Live Lectures With Videos Increased Test Scores", - "publication": "Chronicle of Higher Education", - "publish_date": "October 17, 2012" - }, - { - "title": "Online university to charge tuition fees", - "url": "http://www.bbc.co.uk/news/education-19964787", - "author": "Sean Coughlan", - "image": "bbc_logo_178x138.jpeg", - "deck": null, - "publication": "BBC", - "publish_date": "October 17, 2012" - }, - { - "title": "HarvardX marks the spot", - "url": "http://news.harvard.edu/gazette/story/2012/10/harvardx-marks-the-spot/", - "author": "Tania delLuzuriaga", - "image": "harvardgazette_logo_178x138.jpeg", - "deck": null, - "publication": "Harvard Gazette", - "publish_date": "October 17, 2012" - }, - { - "title": "Harvard EdX Enrolls Near 100000 Students for Free Online Classes", - "url": "http://www.collegeclasses.com/harvard-edx-enrolls-near-100000-students-for-free-online-classes/", - "author": "Keith Koong", - "image": "college_classes_logo_178x138.jpg", - "deck": null, - "publication": "CollegeClasses.com", - "publish_date": "October 17, 2012" - }, - { - "title": "Cengage Learning to Provide Book Content and Pedagogy through edX's Not-for-Profit Interactive Study Via the Web", - "url": "http://www.outsellinc.com/our_industry/headlines/1087978", - "author": null, - "image": "outsell_logo_178x138.jpg", - "deck": null, - "publication": "Outsell.com", - "publish_date": "October 17, 2012" - }, - { - "title": "University of Texas System Embraces MOOCs", - "url": "http://www.usnewsuniversitydirectory.com/articles/university-of-texas-system-embraces-moocs_12713.aspx#.UIBLVq7bNzo", - "author": "Chris Hassan", - "image": "usnews_logo_178x138.jpeg", - "deck": null, - "publication": "US News", - "publish_date": "October 17, 2012" - }, - { - "title": "Texas MOOCs for Credit?", - "url": "http://www.insidehighered.com/news/2012/10/16/u-texas-aims-use-moocs-reduce-costs-increase-completion", - "author": "Steve Kolowich", - "image": "insidehighered_logo_178x138.jpg", - "deck": null, - "publication": "Insider Higher Ed", - "publish_date": "October 16, 2012" - }, - { - "title": "University of Texas Joins Harvard-Founded edX", - "url": "http://www.thecrimson.com/article/2012/10/16/University-of-Texas-edX/", - "author": "Kevin J. Wu", - "image": "harvardcrimson_logo_178x138.jpeg", - "deck": null, - "publication": "The Crimson", - "publish_date": "October 16, 2012" - }, - { - "title": "Entire UT System to join edX", - "url": "http://tech.mit.edu/V132/N45/edx.html", - "author": "Ethan A. Solomon", - "image": "thetech_logo_178x138.jpg", - "deck": null, - "publication": "The Tech", - "publish_date": "October 16, 2012" - }, - { - "title": "First University System Joins edX Platform", - "url": "http://www.govtech.com/education/First-University-System-Joins-edX-platform.html", - "author": "Tanya Roscoria", - "image": "govtech_logo_178x138.jpg", - "deck": null, - "publication": "GovTech.com", - "publish_date": "October 16, 2012" - }, - { - "title": "University of Texas Joining Harvard, MIT Online Venture", - "url": "http://www.bloomberg.com/news/2012-10-15/university-of-texas-joining-harvard-mit-online-venture.html", - "author": "David Mildenberg", - "image": "bloomberg_logo_178x138.jpeg", - "deck": null, - "publication": "Bloomberg", - "publish_date": "October 15, 2012" - }, - { - "title": "University of Texas Joining Harvard, MIT Online Venture", - "url": "http://www.businessweek.com/news/2012-10-15/university-of-texas-joining-harvard-mit-online-venture", - "author": "David Mildenberg", - "image": "busweek_logo_178x138.jpg", - "deck": null, - "publication": "Business Week", - "publish_date": "October 15, 2012" - }, - { - "title": "Univ. of Texas joins online course program edX", - "url": "http://news.yahoo.com/univ-texas-joins-online-course-program-edx-172202035--finance.html", - "author": "Chris Tomlinson", - "image": "ap_logo_178x138.jpg", - "deck": null, - "publication": "Associated Press", - "publish_date": "October 15, 2012" - }, - { - "title": "U. of Texas Plans to Join edX", - "url": "http://www.insidehighered.com/quicktakes/2012/10/15/u-texas-plans-join-edx", - "author": null, - "image": "insidehighered_logo_178x138.jpg", - "deck": null, - "publication": "Inside Higher Ed", - "publish_date": "October 15, 2012" - }, - { - "title": "U. of Texas System Is Latest to Sign Up With edX for Online Courses", - "url": "http://chronicle.com/blogs/wiredcampus/u-of-texas-system-is-latest-to-sign-up-with-edx-for-online-courses/40440", - "author": "Alisha Azevedo", - "image": "chroniclehighered_logo_178x138.jpeg", - "deck": null, - "publication": "Chronicle of Higher Education", - "publish_date": "October 15, 2012" - }, - { - "title": "First University System Joins edX", - "url": "http://www.centerdigitaled.com/news/First-University-System-Joins-edX.html", - "author": "Tanya Roscoria", - "image": "center_digeducation_logo_178x138.jpg", - "deck": null, - "publication": "Center for Digital Education", - "publish_date": "October 15, 2012" - }, - { - "title": "University of Texas Joins Harvard, MIT in edX Online Learning Venture", - "url": "http://harvardmagazine.com/2012/10/university-of-texas-joins-harvard-mit-edx", - "author": null, - "image": "harvardmagazine_logo_178x138.jpeg", - "deck": null, - "publication": "Harvard Magazine", - "publish_date": "October 15, 2012" - }, - { - "title": "University of Texas joins edX", - "url": "http://www.masshightech.com/stories/2012/10/15/daily13-University-of-Texas-joins-edX.html", - "author": "Don Seiffert", - "image": "masshightech_logo_178x138.jpg", - "deck": null, - "publication": "MassHighTech", - "publish_date": "October 15, 2012" - }, - { - "title": "UT System to Forge Partnership with EdX", - "url": "http://www.texastribune.org/texas-education/higher-education/ut-system-announce-partnership-edx/", - "author": "Reeve Hamilton", - "image": "texastribune_logo_178x138.jpg", - "deck": null, - "publication": "Texas Tribune", - "publish_date": "October 15, 2012" - }, - { - "title": "UT System puts $5 million into online learning initiative", - "url": "http://www.statesman.com/news/news/local/ut-system-puts-5-million-into-online-learning-init/nSdd5/", - "author": "Ralph K.M. Haurwitz", - "image": "austin_statesman_logo_178x138.jpg", - "deck": null, - "publication": "The Austin Statesman", - "publish_date": "October 15, 2012" - }, - { - "title": "Harvard’s Online Classes Sound Pretty Popular", - "url": "http://blogs.bostonmagazine.com/boston_daily/2012/10/15/harvards-online-classes-sound-pretty-popular/", - "author": "Eric Randall", - "image": "bostonmag_logo_178x138.jpg", - "deck": null, - "publication": "Boston Magazine", - "publish_date": "October 15, 2012" - }, - { - "title": "Harvard Debuts Free Online Courses", - "url": "http://www.ibtimes.com/harvard-debuts-free-online-courses-846629", - "author": "Eli Epstein", - "image": "ibtimes_logo_178x138.jpg", - "deck": null, - "publication": "International Business Times", - "publish_date": "October 15, 2012" - }, - { - "title": "UT System Joins Online Learning Effort", - "url": "http://www.texastechpulse.com/ut_system_joins_online_learning_effort/s-0045632.html", - "author": null, - "image": "texaspulse_logo_178x138.jpg", - "deck": null, - "publication": "Texas Tech Pulse", - "publish_date": "October 15, 2012" - }, - { - "title": "University of Texas Joins edX", - "url": "http://www.onlinecolleges.net/2012/10/15/university-of-texas-joins-edx/", - "author": "Alex Wukman", - "image": "online_colleges_logo_178x138.jpg", - "deck": null, - "publication": "Online Colleges.net", - "publish_date": "October 15, 2012" - }, - { - "title": "100,000 sign up for first Harvard online courses", - "url": "http://www.masslive.com/news/index.ssf/2012/10/100000_sign_up_for_first_harva.html", - "author": null, - "image": "ap_logo_178x138.jpg", - "deck": null, - "publication": "Associated Press", - "publish_date": "October 15, 2012" - }, - { - "title": "In the new Listener, on sale from 14.10.12", - "url": "http://www.listener.co.nz/commentary/the-internaut/in-the-new-listener-on-sale-from-14-10-12/", - "author": null, - "image": "nz_listener_logo_178x138.jpg", - "deck": null, - "publication": "The Listener", - "publish_date": "October 14, 2012" - }, - { - "title": "HarvardX Classes to Begin Tomorrow", - "url": "http://www.thecrimson.com/article/2012/10/14/harvardx-classes-start-tomorrow/", - "author": "Hana N. Rouse", - "image": "harvardcrimson_logo_178x138.jpeg", - "deck": null, - "publication": "The Crimson", - "publish_date": "October 14, 2012" - }, - { - "title": "Online Harvard University courses draw well", - "url": "http://bostonglobe.com/metro/2012/10/14/harvard-launching-free-online-courses-sign-for-first-two-classes/zBDuHY0zqD4OESrXWfEgML/story.html", - "author": "Brock Parker", - "image": "bostonglobe_logo_178x138.jpeg", - "deck": null, - "publication": "Boston Globe", - "publish_date": "October 14, 2012" - }, - { - "title": "Harvard ready to launch its first free online courses Monday", - "url": "http://www.boston.com/yourtown/news/cambridge/2012/10/harvard_ready_to_launch_its_fi.html", - "author": "Brock Parker", - "image": "bostonglobe_logo_178x138.jpeg", - "deck": null, - "publication": "Boston Globe", - "publish_date": "October 12, 2012" - }, - { - "title": "edX: Harvard's New Domain", - "url": "http://www.thecrimson.com/article/2012/10/4/edx-scrutiny-online-learning/ ", - "author": "Delphine Rodrik and Kevin Su", - "image": "harvardcrimson_logo_178x138.jpeg", - "deck": null, - "publication": "The Crimson", - "publish_date": "October 4, 2012" - }, - { - "title": "New Experiments in the edX Higher Ed Petri Dish", - "url": "http://www.nonprofitquarterly.org/policysocial-context/21116-new-experiments-in-the-edx-higher-ed-petri-dish.html", - "author": "Michelle Shumate", - "image": "npq_logo_178x138.jpg", - "deck": null, - "publication": "Non-Profit Quarterly", - "publish_date": "October 4, 2012" - }, - { - "title": "What Campuses Can Learn From Online Teaching", - "url": "http://online.wsj.com/article/SB10000872396390444620104578012262106378182.html?mod=googlenews_wsj", - "author": "Rafael Reif", + "title": "Online Education Lifts Pass Rates at University", + "url": "http://online.wsj.com/article/SB10001424127887323741004578414861572832182.html?mod=googlenews_wsj", + "author": "Geoffrey Fowler", "image": "wsj_logo_178x138.jpg", "deck": null, - "publication": "Wall Street Journal", - "publish_date": "October 2, 2012" + "publication": "The Wall Street Journal", + "publish_date": "April 10, 2013" }, + { - "title": "MongoDB courses to be offered via edX", - "url": "http://tech.mit.edu/V132/N42/edxmongodb.html", - "author": "Jake H. Gunter", - "image": "thetech_logo_178x138.jpg", + "title": "Software Seen Giving Grades on Essay Tests", + "url": "http://www.nytimes.com/2013/04/05/science/new-test-for-computers-grading-essays-at-college-level.html?pagewanted=all&_r=0", + "author": "John Markoff", + "image": "nyt_logo_178x138.jpeg", "deck": null, - "publication": "The Tech", - "publish_date": "October 2, 2012" + "publication": "The New York Times", + "publish_date": "April 4, 2013" }, + { - "title": "5 Ways That edX Could Change Education", - "url": "http://chronicle.com/article/5-Ways-That-edX-Could-Change/134672/", - "author": "Marc Parry", - "image": "chroniclehighered_logo_178x138.jpeg", + "title": "Stanford to help build edX MOOC platform", + "url": "http://www.washingtonpost.com/local/education/stanford-to-help-build-edx-mooc-platform/2013/04/02/5b53bb3e-9bbe-11e2-9a79-eb5280c81c63_story.html", + "author": "Nick Anderson", + "image": "wash_post_logo_178x138.jpg", "deck": null, - "publication": "Chronicle of Higher Education", - "publish_date": "October 1, 2012" + "publication": "The Washington Post", + "publish_date": "April 3, 2013" }, + { - "title": "MIT profs wait to teach you, for free", - "url": "http://www.dnaindia.com/mumbai/report_mit-profs-wait-to-teach-you-for-free_1747273", - "author": "Kanchan Srivastava", - "image": "dailynews_india_logo_178x138.jpg", + "title": "Could online ed end college as we know it?", + "url": "http://www.cbsnews.com/video/watch/?id=50143164n", + "author": "CBS This Morning", + "image": "cbsnews_178x138.jpg", "deck": null, - "publication": "Daily News and Analysis India", - "publish_date": "October 1, 2012" + "publication": "CBS Television Network", + "publish_date": "March 19, 2013" }, + + { + "title": "The Professors’ Big Stage", + "url": "http://www.nytimes.com/2013/03/06/opinion/friedman-the-professors-big-stage.html?_r=1&#commentsContainer", + "author": "Thomas L. Friedman", + "image": "nyt_logo_178x138.jpeg", + "deck": null, + "publication": "The New York Times", + "publish_date": "March 6, 2013" + }, + + + { + "title": "Universities Abroad Join Partnerships On the Web", + "url": "http://www.nytimes.com/2013/02/21/education/universities-abroad-join-mooc-course-projects.html", + "author": "Tamar Lewin", + "image": "nyt_logo_178x138.jpeg", + "deck": null, + "publication": "The New York Times", + "publish_date": "February 20, 2013" + }, + + + { + "title": "Georgetown to offer free online courses", + "url": "http://www.washingtonpost.com/local/education/georgetown-to-offer-free-online-courses/2012/12/09/365c4612-3fd3-11e2-bca3-aadc9b7e29c5_story.html", + "author": "Nick Anderson", + "image": "wash_post_logo_178x138.jpg", + "deck": null, + "publication": "The Washington Post", + "publish_date": "December 9, 2012" + }, + + { + "title": "Wellesley College teams up with online provider edX", + "url": "http://bostonglobe.com/2012/12/04/edx/AqnQ808q4IEcaUa8KuZuBO/story.html", + "author": "Peter Schworm", + "image": "bostonglobe_logo_178x138.jpeg", + "deck": null, + "publication": "The Boston Globe", + "publish_date": "December 4, 2012" + }, + { "title": "The Year of the MOOC", "url": "http://www.nytimes.com/2012/11/04/education/edlife/massive-open-online-courses-are-multiplying-at-a-rapid-pace.html", 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
Staff Debug Info
@@ -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/contact.html b/lms/templates/static_templates/contact.html index d848164720..79e2743dbc 100644 --- a/lms/templates/static_templates/contact.html +++ b/lms/templates/static_templates/contact.html @@ -33,6 +33,9 @@

Universities

If you are a university wishing to collaborate or with questions about edX, please email university@edx.org.

+

Accessibility

+

EdX strives to create an innovative online-learning platform that promotes accessibility for everyone, including students with disabilities. We are dedicated to improving the accessibility of the platform and welcome your comments or questions at accessibility@edx.org.

+ diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html index e33ff62e9a..18ef1119e1 100644 --- a/lms/templates/static_templates/jobs.html +++ b/lms/templates/static_templates/jobs.html @@ -75,19 +75,19 @@
-

DIRECTOR OF EDUCATIONAL SERVICES

-

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

+

DIRECTOR OF EDUCATION SERVICES

+

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

  1. Delivering 20 new courses in 2013 in collaboration with the partner Universities
      -
    • Reporting to the Director of Educational Services are the Video production team, responsible for post-production of Course Video. The Director must understand how to balance artistic quality and learning objectives, and reduce production time so that video capabilities are readily accessible and at reasonable costs.

      -
    • Reporting to the Director are a small team of Program Managers, who are responsible for managing the day to day of course production and operations. The Director must be experienced in capacity planning and operations, understand how to deploy lean collaboration and able to build alliances inside edX and the University. In conjunction with the Program Managers, the Director of Educational Services will supervise the collection of research, the retrospectives with Professors and the assembly of best practices in course production and operations. The three key deliverables are the use of a well-defined lean process for onboarding Professors, the development of tracking tools, and assessment of effectiveness of Best Practices. -
    • Also reporting to the Director of Education Services are content engineers and Course Fellows, skilled in the development of edX assessments. The Director of Educational Services will also be responsible for communicating to the VP of Engineering requirements for new types of course assessments. Course Fellows are extremely talented Ph.D.’s who work directly with the Professors to define and develop assessments and course curriculum.
    • +
    • Reporting to the Director of Education Services are the Video production team, responsible for post-production of Course Video. The Director must understand how to balance artistic quality and learning objectives, and reduce production time so that video capabilities are readily accessible and at reasonable costs.

      +
    • Reporting to the Director are a small team of Program Managers, who are responsible for managing the day to day of course production and operations. The Director must be experienced in capacity planning and operations, understand how to deploy lean collaboration and able to build alliances inside edX and the University. In conjunction with the Program Managers, the Director of Education Services will supervise the collection of research, the retrospectives with Professors and the assembly of best practices in course production and operations. The three key deliverables are the use of a well-defined lean process for onboarding Professors, the development of tracking tools, and assessment of effectiveness of Best Practices. +
    • Also reporting to the Director of Education Services are content engineers and Course Fellows, skilled in the development of edX assessments. The Director of Education Services will also be responsible for communicating to the VP of Engineering requirements for new types of course assessments. Course Fellows are extremely talented Ph.D.’s who work directly with the Professors to define and develop assessments and course curriculum.
  2. Training and Onboarding of 30 Partner Universities and Affiliates
      -
    • The edX Director of Educational Services is responsible for building out the Training capabilities and delivery mechanisms for onboarding Professors at partner Universities. The edX Director must build out both the Training Team and the curriculum. Training will be delivered in both online courses, self-paced formats, and workshops. The training must cover a curriculum that enables partner institutions to be completely independent. Additionally, partner institutions should be engaged to contribute to the curriculum and partner with edX in the delivery of the material. The curriculum must exemplify the best in online learning, so the Universities are inspired to offer the kind of learning they have experienced in their edX Training.
    • +
    • The edX Director of Education Services is responsible for building out the Training capabilities and delivery mechanisms for onboarding Professors at partner Universities. The edX Director must build out both the Training Team and the curriculum. Training will be delivered in both online courses, self-paced formats, and workshops. The training must cover a curriculum that enables partner institutions to be completely independent. Additionally, partner institutions should be engaged to contribute to the curriculum and partner with edX in the delivery of the material. The curriculum must exemplify the best in online learning, so the Universities are inspired to offer the kind of learning they have experienced in their edX Training.
    • Expand and extend the education goals of the partner Universities by operationalizing best practices.
    • Engage with University Boards to design and define the success that the technology makes possible.
    @@ -117,7 +117,7 @@
  3. Develop team skills in a ferociously intelligent group
  4. Fan the enthusiasm of the partner Universities when the enormity of the transition they are facing becomes intimidating
  5. Encourage creativity to allow the technology to provoke pedagogical possibilities that brick and mortar classes have precluded.
  6. -
  7. Lean and Agile thinking and training. Experienced in scrum or kanban.
  8. +
  9. Lean and Agile thinking and training. Experienced in Scrum or Kanban.
  10. Design and deliver hiring/development plans which meet rapidly changing skill needs.
  11. @@ -128,10 +128,10 @@

    MANAGER OF TRAINING SERVICES

    -

    The Manager of Training Services is an integral member of the edX team, a leader who is also a doer, working hands-on in the development and delivery of edX’s training portfolio. Reporting to the Director of Educational Services, the manager will be a strategic thinker, providing leadership and vision in the development of world-class training solutions tailored to meet the diverse needs of edX Universities, partners and stakeholders

    +

    The Manager of Training Services is an integral member of the edX team, a leader who is also a doer, working hands-on in the development and delivery of edX’s training portfolio. Reporting to the Director of Education Services, the manager will be a strategic thinker, providing leadership and vision in the development of world-class training solutions tailored to meet the diverse needs of edX Universities, partners and stakeholders

    Responsibilities:

      -
    • Working with the Director of Educational Services, create and manage a world-class training program that includes in-person workshops and online formats such as self-paced courses, and webinars.
    • +
    • Working with the Director of Education Services, create and manage a world-class training program that includes in-person workshops and online formats such as self-paced courses, and webinars.
    • Work across a talented team of product developers, video producers and content experts to identify training needs and proactively develop training curricula for new products and services as they are deployed.
    • Develop the means for sharing and showcasing edX best practices for both internal and external audiences.
    • Apply sound instructional design theory and practice in the development of all edX training resources.
    • @@ -150,7 +150,7 @@

      Requirements:

      • Minimum of 5-7 years experience developing and delivering educational training, preferably in an educational technology organization.
      • -
      • Lean and Agile thinking and training. Experienced in Scrum or kanban.
      • +
      • Lean and Agile thinking and training. Experienced in Scrum or Kanban.
      • Excellent interpersonal skills including proven presentation and facilitation skills.
      • Strong oral and written communication skills.
      • Proven experience with production and delivery of online training programs that utilize asychronous and synchronous delivery mechanisms.
      • @@ -159,13 +159,50 @@
      • Proactive, optimistic approach to problem solving.
      • Commitment to constant personal and organizational improvement.
      • Willingness to travel to partner sites as needed.
      • -
      • Bachelors required, Master’s in Education, organizational learning, or other related field preferred.
      • +
      • Bachelor's 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.

    + +
    +
    +

    TRAINER

    +

    All those Universities on the edX homepage are full of incredible professors and teaching teams, designing on-line courses that will change the face of education. The edX team is constantly training whole new university teams on how to make their visions shine using edX software. We’re looking for some truly talented people to help train people on the tools that are enabling the future of education.

    +

    Responsibilities:

    +
      +
    • Facilitate training programs as required ensuring that best practices are incorporated in all learning environments.
    • +
    • Create and design learning materials for training curriculums, incorporate edX best practices into training curriculum.
    • +
    • Incorporate key performance metrics into training modules; participate in strategic initiatives
    • +
    • Measure, monitor and share training results with business units to identify future training opportunities.
    • +
    • Identify and leverage existing resources to maximize partner efficiency and productivity.
    • +
    • Work with both Universities and edX to provide strategic input based on future training needs.
    • +
    • Communicate effectively in oral and written presentations.
    • +
    • Analyze learners training needs and identify cross training opportunities.
    • +
    • Mentor and train others on training tools to expand training efficiency and uniformity.
    • +
    • Build relationships with universities to be viewed as a trusted training partner.
    • +
    +

    Requirements:

    +
      +
    • Minimum of 1-3 years experience developing and delivering educational training, preferably in an educational technology organization.
    • +
    • Lean and Agile thinking and training. Experienced in Scrum or Kanban preferred.
    • +
    • 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.
    • +
    • Bachelors or Master’s in Education, organizational learning, instructional design 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.

    +
    +
    + +

    INSTRUCTIONAL DESIGNER

    @@ -187,10 +224,12 @@

    Qualifications:

      -
    • Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable.
    • -
    • Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential.
    • Ability to meet deadlines and manage expectations of constituents. +
    • Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment preferable. 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.
    • +
    • Experience in higher education with additional experience in a start-up or research environment preferable.
    • +
    • Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential.
    • +
    • Ability to meet deadlines and manage expectations of constituents.
    • Capacity to develop new and relevant technology skills. Experience using game theory design and learning analytics to inform instructional design decisions and strategy.
    • -
    • Technical Skills: Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.
    • +
    • Technical Skills: Video and screencasting experience. LMS Platform experience, XML, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.

    Eligible candidates will be invited to respond to an Instructional Design task based on current or future edX course development needs.

    @@ -202,19 +241,19 @@

    PROGRAM MANAGER

    -

    edX Program Managers (PM) lead the edX's course production process. They are systems thinkers who manage the creation of a course from start to finish. PMs work with University Professors and course staff to help them take advantage of edX services to create world class online learning offerings and encourage the exploration of an emerging form of higher education.

    +

    EdX Program Managers (PM) lead the edX's course production process. They are systems thinkers who manage the creation of a course from start to finish. PMs work with University Professors and course staff to help them take advantage of edX services to create world class online learning offerings and encourage the exploration of an emerging form of higher education.

    Responsibilities:

      -
    • Create and execute the course production cycle. PMs are able to examine and explain what they do in great detail and able to think abstractly about people, time, and processes. They coordinate the efforts of multiple
    • teams engaged in the production of the courses assigned to them. +
    • Create and execute the course production cycle. PMs are able to examine and explain what they do in great detail and able to think abstractly about people, time, and processes. They coordinate the efforts of multiple teams engaged in the production of the courses assigned to them.
    • Train partners and drive best practices adoption. PMs train course staff from partner institutions and help them adopt best practices for workflow and tools.
    • Build capacity. Mentor staff at partner institutions, train the trainers that help them scale their course production ability.
    • -
    • Create visibility. PMs are responsible for making the state of the course production system accessible and comprehensible to all stakeholders. They are capable of training Course development teams in Scrum and
    • Kanban, and are Lean thinkers and educators. +
    • Create visibility. PMs are responsible for making the state of the course production system accessible and comprehensible to all stakeholders. They are capable of training Course development teams in Scrum and Kanban, and are Lean thinkers and educators.
    • Improve workflows. PMs are responsible for carefully assessing the methods and outputs of each course and adjusting them to take best advantage of available resources.
    • Encourage innovation. Spark creativity in course teams to build new courses that could never be produced in brick and mortar settings.

    Qualifications:

      -
    • Bachelor's Degree. Master's Degree preferred.
    • +
    • Bachelor's Degree. Master's Degree 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.
    • At least 2 years of experience working with University faculty and administrators.
    • Proven record of successful Scrum or Kanban project management, including use of project management tools.
    • Ability to create processes that systematically provide solutions to open ended challenges.
    • @@ -237,36 +276,6 @@
    -
    -
    -

    PROJECT MANAGER (PMO)

    -

    As a fast paced, rapidly growing organization serving the evolving online higher education market, edX maximizes its talents and resources. To help make the most of this unapologetically intelligent and dedicated team, we seek a project manager to increase the accuracy of our resource and schedule estimates and our stakeholder satisfaction.

    -

    Responsibilities:

    -
      -
    • Coordinate multiple projects to bring Courses, Software Product and Marketing initiatives to market, all of which are related, which have both dedicated and shared resources.
    • -
    • Provide, at a moment’s notice, the state of development, so that priorities can be enforced or reset, so that future expectations can be set accurately.
    • -
    • Develop lean processes that supports a wide variety of efforts which draw on a shared resource pool.
    • -
    • Develop metrics on resource use that support the leadership team in optimizing how they respond to unexpected challenges and new opportunities.
    • -
    • Accurately and clearly escalate only those issues which need escalation for productive resolution. Assist in establishing consensus for all other issues.
    • -
    • Advise the team on best practices, whether developed internally or as industry standards.
    • -
    • Recommend to the leadership team how to re-deploy key resources to better match stated priorities.
    • -
    • Help the organization deliver on its commitments with more consistency and efficiency. Allow the organization to respond to new opportunities with more certainty in its ability to forecast resource needs.
    • -
    • Select and maintain project management tools for Scrum and Kanban that can serve as the standard for those we use with our partners.
    • -
    • Forecast future resource needs given the strategic direction of the organization.
    • -
    -

    Skills:

    -
      -
    • Bachelor’s degree or higher
    • -
    • Exquisite communication skills, especially listening
    • -
    • Inexhaustible attention to detail with the ability to let go of perfection
    • -
    • Deep commitment to Lean project management, including a dedication to its best intentions not just its rituals
    • -
    • Sense of humor and humility
    • -
    • Ability to hold on to the important in the face of the urgent
    • -
    -

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

    -
    -
    -
    @@ -286,8 +295,7 @@

    Qualifications:

      -
    • Bachelor’s degree or higher in a Technical Area
    • -
    • MBA or Masters in Design preferred
    • +
    • Bachelor’s degree or higher in a Technical Area, MBA or Masters in Design 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.
    • Proven ability to develop and implement strategy
    • Exquisite organizational skills
    • Deep analytical skills
    • @@ -309,8 +317,8 @@

      Content engineers help create the technology for specific courses. The tasks include:

      • Developing of course-specific user-facing elements, such as the circuit editor and simulator.
      • -
      • Integrating course materials into courses
      • -
      • Creating programs to grade questions designed with complex technical features
      • +
      • Integrating course materials into courses.
      • +
      • Creating programs to grade questions designed with complex technical features.
      • Knowledge of Python, XML, and/or JavaScript is desired. Strong interest and background in pedagogy and education is desired as well.
      • Building course components in straight XML or through our course authoring tool, edX Studio.
      • Assisting University teams and in house staff take advantage of new course software, including designing and developing technical refinements for implementation.
      • @@ -319,14 +327,14 @@

      Qualifications:

        -
      • Bachelor’s degree or higher
      • -
      • Thorough knowledge of Python, DJango, XML,HTML, CSS , Javascript and backbone.js
      • -
      • Ability to work on multiple projects simultaneously without splintering
      • +
      • Bachelor’s degree or higher. 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.
      • +
      • Thorough knowledge of Python, DJango, XML, HTML, CSS, JavaScript and backbone.js.
      • +
      • Ability to work on multiple projects simultaneously without splintering.
      • Tactfully escalate conflicting deadlines or priorities only when needed. Otherwise help the team members negotiate a solution.
      • Unfailing attention to detail, especially the details the course teams have seen so often they don’t notice them anymore.
      • -
      • Readily zoom from the big picture to the smallest course component to notice when typos, inconsistencies or repetitions have unknowingly crept in
      • +
      • Readily zoom from the big picture to the smallest course component to notice when typos, inconsistencies or repetitions have unknowingly crept in.
      • Curiosity to step into the shoes of an online student working to master the course content.
      • -
      • Solid interpersonal skills, especially good listening
      • +
      • Solid interpersonal skills, especially good listening.

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

      @@ -337,7 +345,7 @@

      SOFTWARE ENGINEER

      -

      edX is looking for engineers who can contribute to its Open Source learning platform. We are a small team with a startup, lean culture, committed to building open-source software that scales and dramatically changes the face of education. Our ideal candidates are hands on developers who understand how to build scalable, service based systems, preferably in Python and have a proven track record of bringing their ideas to market. We are looking for engineers with all levels of experience, but you must be a proven leader and outstanding developer to work at edX.

      +

      EdX is looking for engineers who can contribute to its Open Source learning platform. We are a small team with a startup, lean culture, committed to building open-source software that scales and dramatically changes the face of education. Our ideal candidates are hands on developers who understand how to build scalable, service based systems, preferably in Python and have a proven track record of bringing their ideas to market. We are looking for engineers with all levels of experience, but you must be a proven leader and outstanding developer to work at edX.

      There are a number of projects for which we are recruiting engineers:
      @@ -352,14 +360,14 @@

      Requirements:

      • Real-world experience with Python or other dynamic development languages.
      • -
      • Able to code front to back, including HTML, CSS, Javascript, Django, Python
      • -
      • You must be committed to an agile development practices, in Scrum or Kanban
      • -
      • Demonstrated skills in building Service based architecture
      • -
      • Test Driven Development
      • -
      • Committed to Documentation best practices so your code can be consumed in an open source environment
      • -
      • Contributor to or consumer of Open Source Frameworks
      • -
      • BS in Computer Science from top-tier institution
      • -
      • Acknowledged by peers as a technology leader
      • +
      • Able to code front to back, including HTML, CSS, JavaScript, Django, Python.
      • +
      • You must be committed to an agile development practices, in Scrum or Kanban.
      • +
      • Demonstrated skills in building Service based architecture.
      • +
      • Test Driven Development.
      • +
      • Committed to Documentation best practices so your code can be consumed in an open source environment.
      • +
      • Contributor to or consumer of Open Source Frameworks.
      • +
      • BS in Computer Science from top-tier institution. 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.
      • +
      • Acknowledged by peers as a technology leader.

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

      @@ -367,6 +375,177 @@
      +
      +
      +

      DEVOPS ENGINEER – SYSTEMS ADMINISTRATOR

      +

      The Devop Engineers at edX help develop and maintain the infrastructure in AWS for all services and systems required to run edX. We're seeking a capable systems administrator who is unafraid of scripting languages and development to build out tools in order to improve the functionality of edX. The devops team primarily focuses on the provisioning, configuration, and deployment of services at edX. If you have a passion for automation and constant improvement then we want to hear from you. Our production environment is primarily built on Ubuntu (in AWS) and we use Puppet and Fabric to manage most of the environment.

      +

      In addition to the primary task of building infrastructure the Devops team supports the developers in a variety of other contexts, including helping with desktop development environments if required. We participate in on-call and emergency support and there will be occasional out of normal hours work required.

      +

      Responsibilities:

      +
        +
      • Work with developers and staff to maintain and improve the infrastructure of edX.
      • +
      • Assist where needed with other technical support tasks to support the fast moving pace of edX.
      • +
      • Rapidly diagnose and resolve faults with organization-wide servers and services, and communicate to users as appropriate.
      • +
      +

      Requirements:

      +
        +
      • Bachelor's degree in engineering or computer science. 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.
      • +
      • Three or more years of systems administration.
      • +
      • Must have an excellent working knowledge of Linux both as an end-user and as an administrator.
      • +
      • Must be adept in programming/scripting languages such as Python, Ruby, Bash.
      • +
      • Must be familiar with a configuration management system such as Puppet, Chef, Ansible.
      • +
      • Must have experience running web applications in a production environment.
      • +
      • Must have excellent personal interaction skills as the position requires interfacing with a wide range of people up to board level.
      • +
      • Ideally possesses experience with some of the following technologies: nginx, mysql, mongodb, django environments, splunk, git.
      • +
      + +

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

      +
      +
      + + +
      +
      +

      LEARNING SCIENCES ENGINEER

      +

      In 2012, edX reinvented education. In 2013, the edX learning sciences team is charged with reinventing education, again. The goal of the team is to prototype and develop technologies which will radically change the way students learn and instructors teach. We will engage in projects in learning analytics, crowdsourced content development, intelligent tutoring, as well as radical changes to the ways course content is structured. We are looking to opportunistically build a small (3 person), fast-moving team capable of rapidly bringing advanced development projects to prototype and to market. All members of the team must be spectacular software engineers capable of working in or adapting to dynamic, duck typed, functional languages (Python and JavaScript). In addition, we are looking for some combination of:

      +
        +
      • Deep expertise in mathematics, and in particular, advanced linear algebra, machine learning, big data, psychometrics, and probability.
      • +
      • UX design. Capable of envisioning user interface for software that does things that have never been done before, and bringing them through to market. Skills should be broad and range the full gamut: graphic design, UX, HTML5, basic JavaScript, and CSS.
      • +
      • Interest and experience in both research and practice of education, cognitive science, and related fields.
      • +
      • Core backend experience (Python, Django, MongoDB, SQL)
      • +
      • Background in social networks and social network analysis (both social science and mathematics) is desirable as well.
      • +
      +

      More than anything, we’re looking for spectacular people capable of very rapidly building things which have never been built before. We’re capable of providing both traditional employment, and potentially, in partnership with MIT, more academic opportunities.

      + +

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

      +
      +
      + +
      +
      +

      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.

      +
      +
      + +
      +
      +

      WEB DESIGNER, PRODUCT TEAM

      + +

      EdX is looking for a Web Designer to join our Product Team and shape the experience of edX's online learning tools. With thousands and thousands of students and hundreds of professors using our software every day, our online learning tools have to sing. Our ideal candidates are passionate and picky about what makes a good user experience; sweat the mechanical, visual, and transactional details when designing; know how to bring an idea or project from a sketch on paper to being alive in a browser; can instinctually bring organization to a design meeting, deliverable, or project; and thrive on collaboration with colleagues and constant iteration/refinement.

      + +

      As an edX Designer, you:

      +
        +
      • Have an innate sense of – and strong opinion about – good usability when it comes to web applications, and an ability to clearly articulate both.
      • +
      • Understand established interactive technologies and possess an undying thirst to learn about new ones.
      • +
      • Define and work within visual themes based on your excellent understanding of grids, typography, color, and design principles.
      • +
      • Marry design aesthetics to user experiences while keeping in mind accessibility, usability, and web standards.
      • +
      • Can use HTML5, CSS3, and DOM-manipulating JavaScript to represent your designs in the browser.
      • +
      • Conceptualize and articulate complex ideas to drive decisions, facilitate understanding, and reach consensus.
      • +
      • Document your thinking using appropriately chosen, informed deliverables such as sketches, wireframes, prototypes, site maps/flows, personas, style tiles, and design comps.
      • +
      • Have a perfectionist mindset, but won’t lose momentum in projects because of it.
      • +
      • Expertly present user experience and design recommendations to team members.
      • +
      + +

      Requirements:

      +
        +
      • Have at least 2 years of professional, post-collegiate experience.
      • +
      • Have a BA, BS, BFA, or equivalent work experience in areas such as human-computer interaction, information science, graphic or industrial design, computer science, fine arts, social sciences such as psychology, or another related field. 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.
      • +
      +

      About the Product Design Team:

      +

      We are a small team with a startup, lean culture, committed to building tools that help our users learn and teach online. Working alongside developers, course staff, product owners, and project stakeholders, our Designers shepherd the experience of an idea or tool through research and strategy phases and lead the Information Architecture, Interaction Design, Visual Design, and Front End Development efforts in bringing that experience to life. We enjoy holding Design Studio exercises, finding the right design tool to do the job efficiently, and our CSS preprocessors.

      + +

      If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Designer role at edX, and online samples of your work to jobs@edx.org. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.

      +
      +
      + +
      +
      +

      FRONT END DEVELOPER

      +

      edX is looking for a Front End Developer to join our Product and Engineering Teams to shape the experience of all of edX's online learning tools. Thousands of students learn with us every day – the way they connect with their courses, their professors and edX is through our ever more powerful front end. Our ideal candidates not only know modern front end development best practices, but make organization standards and teach others with them; sweat the mechanical, visual, and transactional details when bring a design to life in the browser; can instinctually bring organization to their HTML/CSS/JavaScript, documentation, or project; and thrive on collaborating with both designers and developers throughout a project's lifecycle.

      +

      As an edX Front End Developer, you:

      +
        +
      • Translate flat design comps, wireframes, and prototypes to production-ready interactive interfaces with joy and passion.
      • +
      • Are very familiar with cutting-edge front-end development practices and technology (CSS3, media queries, responsive web design, HTML5, etc.).
      • +
      • Write JavaScript without the use of a library while still being familiar with popular libraries such as jQuery.
      • +
      • Can abstract layouts, design patterns, and UI components while building out the interface to a product or application.
      • +
      • Appreciate that web standards, accessibility, and usability are essential to uphold.
      • +
      • Generally have experience with server-side templating and data extraction code while enjoying learning more from the development team.
      • +
      • Maintain the sanctity of a project's information architecture, interaction design, and visual design details while contributing to the effort.
      • +
      • Know how to test and refactor your code across browsers and with QA teams.
      • +
      • Work well with designers, developers, and colleagues.
      • +
      • Take pride in your communicative and collaborative abilities.
      • +
      + +

      Front End Developers must also:

      +
        +
      • Have at least two years of professional, post-collegiate experience.
      • +
      • Have a BS, BFA or equivalent work experience. 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.
      • +
      + +

      About the Product Design and Development Teams:

      +

      We are a small team with a startup, lean culture, committed to building tools that help our users learn and teach online. Working alongside developers, course staff, product owners, and project stakeholders, our Designers shepherd the experience of an idea or tool through research and strategy phases and lead the Information Architecture, Interaction Design, Visual Design, and Front End Development efforts in bringing that experience to life.

      + +

      If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Front End Developer role at edX, and online samples of your work to jobs@edx.org. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.

      +
      +
      +
      @@ -374,12 +553,17 @@

      How to Apply

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

      diff --git a/lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html b/lms/templates/static_templates/press_releases/cengage_to_provide_book_content.html similarity index 100% rename from lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html rename to lms/templates/static_templates/press_releases/cengage_to_provide_book_content.html diff --git a/lms/templates/static_templates/press_releases/edX_announces_proctored_exam_testing.html b/lms/templates/static_templates/press_releases/edx_announces_proctored_exam_testing.html similarity index 100% rename from lms/templates/static_templates/press_releases/edX_announces_proctored_exam_testing.html rename to lms/templates/static_templates/press_releases/edx_announces_proctored_exam_testing.html diff --git a/lms/templates/static_templates/press_releases/Elsevier_collaborates_with_edX.html b/lms/templates/static_templates/press_releases/elsevier_collaborates_with_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/Elsevier_collaborates_with_edX.html rename to lms/templates/static_templates/press_releases/elsevier_collaborates_with_edx.html diff --git a/lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html b/lms/templates/static_templates/press_releases/gates_foundation_announcement.html similarity index 100% rename from lms/templates/static_templates/press_releases/Gates_Foundation_announcement.html rename to lms/templates/static_templates/press_releases/gates_foundation_announcement.html diff --git a/lms/templates/static_templates/press_releases/Georgetown_joins_edX.html b/lms/templates/static_templates/press_releases/georgetown_joins_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/Georgetown_joins_edX.html rename to lms/templates/static_templates/press_releases/georgetown_joins_edx.html diff --git a/lms/templates/static_templates/press_releases/Lewin_course_announcement.html b/lms/templates/static_templates/press_releases/lewin_course_announcement.html similarity index 100% rename from lms/templates/static_templates/press_releases/Lewin_course_announcement.html rename to lms/templates/static_templates/press_releases/lewin_course_announcement.html diff --git a/lms/templates/static_templates/press_releases/MIT_and_Harvard_announce_edX.html b/lms/templates/static_templates/press_releases/mit_and_harvard_announce_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/MIT_and_Harvard_announce_edX.html rename to lms/templates/static_templates/press_releases/mit_and_harvard_announce_edx.html diff --git a/lms/templates/static_templates/press_releases/Spring_2013_course_announcements.html b/lms/templates/static_templates/press_releases/spring_courses.html similarity index 100% rename from lms/templates/static_templates/press_releases/Spring_2013_course_announcements.html rename to lms/templates/static_templates/press_releases/spring_courses.html diff --git a/lms/templates/static_templates/press_releases/stanford_to_work_with_edx.html b/lms/templates/static_templates/press_releases/stanford_to_work_with_edx.html new file mode 100644 index 0000000000..2b19b62de3 --- /dev/null +++ b/lms/templates/static_templates/press_releases/stanford_to_work_with_edx.html @@ -0,0 +1,92 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform +
      + + +
      +
      +

      Stanford University to Collaborate with edX on Development of Non-Profit Open Source edX Platform

      +
      +
      +

      edX Learning Platform to be open source and available on June 1

      + +

      CAMBRIDGE, MA and STANFORD, CA – April 3, 2013 – + +Stanford University and edX, the not-for-profit online learning enterprise founded by Harvard University and the Massachusetts Institute of Technology (MIT), today announced their collaboration to advance the development of edX’s open source learning platform and provide free and open online learning tools for institutions around the world.

      + +

      As part of this announcement, edX will release the source code for its entire online learning platform on June 1, 2013. In support of that move, Stanford will integrate features of its existing Class2Go platform into the edX platform, use the integration as an internal platform for online coursework for on-campus and distance learners, and work collaboratively with edX and other institutions to further develop the edX platform.

      + +

      “This collaboration brings together two leaders in online education in a common effort to ensure that the world’s universities have the strongest possible not-for-profit, open source platform available to them,” said John Mitchell, vice provost for online learning at Stanford University. “A not-for-profit, open source platform will help universities experiment with different ways to produce and share content, fostering continued innovation through a vibrant community of contributors.”

      + +

      EdX and Stanford will collaborate along with others around the globe on the ongoing development and refinement of the edX online learning platform. As of June 1, developers everywhere will be able to freely access the source code of the edX learning platform, including code for its Learning Management System (LMS); Studio, a course authoring tool; xBlock, an application programming interface (API) for integrating third-party learning objects; and machine grading API’s. EdX will support and nurture the community of developers contributing to the enhancement of the edX platform by providing a rich environment for developer collaboration as well as technical and process guidelines to facilitate developer contributions.

      + +

      “It has been our vision to offer our platform as open source since edX’s founding by Harvard and MIT,” stated Anant Agarwal, president of edX. “We are now realizing that vision, and I am pleased to welcome Stanford University, one of the world’s leading institutions of higher education, to further this global open source solution. I want to acknowledge the key role played by our X Consortium member UC Berkeley, which was instrumental in fostering this collaboration. We believe the edX platform—the Linux of learning—will benefit from all the world’s institutions and communities.”

      + +

      EdX is pursuing an open source vision to enhance access to higher education for the entire world. One of the chief benefits of massive open online courses (MOOCs) is that they bring together a tremendously diverse student body to learn with and from each other. EdX has chosen to extend that perspective to its learning platform as well, knowing that drawing upon the global community of developers is an effective route to both transform and deliver the world’s best and most accessible online and blended learning experience.

      + +

      MOOCs and innovative online teaching approaches on college campuses, such as the “flipped classroom,” use web environments that support interactive video, online discussion, social/cohort interaction, assessment and other functions. Open source online learning platforms will allow universities to develop their own delivery methods, partner with other universities and institutions as they choose, collect data, and control branding of their educational material. Further developing online opportunities through open source technology is a key objective of the partnership between edX and Stanford.

      + +

      Stanford will continue to provide a range of platforms for its instructors to choose from in hosting their online coursework, including continued partnerships with Coursera and other providers. The university will focus its ongoing platform development efforts on the new platform, combining key features from the Class2Go open source platform with the open source edX code base.

      + +

      The edX learning platform source code, as well as platform developments from Stanford, edX and other contributors, will be available on June 1, 2013 and can be accessed from the edX Platform Repository located at https://github.com/edX.

      + + +

      About edX

      + +

      EdX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology focused on transforming online and on-campus learning through groundbreaking methodologies, game-like experiences and cutting-edge research. EdX provides inspirational and transformative knowledge to students of all ages, social status, and income who form worldwide communities of learners. EdX uses its open source technology to transcend physical and social borders. We’re focused on people, not profit. EdX is based in Cambridge, Massachusetts in the USA.

      + +

      About Stanford University

      + +

      +Stanford University is engaged in a variety of efforts to develop online learning – experimenting with coursework for both on-campus and off-campus students, researching key questions around what a digital environment means for teaching and learning, and pursuing platform development. More information on Stanford’s online learning activities is available at http://online.stanford.edu + + +

      +

      Media Contact:

      +

      Dan O'Connell

      +

      oconnell@edx.org

      +

      (617) 480-6585

      +
      + +
      +

      Brad Hayward

      +

      bhayward@stanford.edu

      +

      650-724-0199

      +
      + +
      +

      Lisa Lapin

      +

      lapin@stanford.edu

      +

      650-725-8396

      +
      + + + +
      +
      +
      diff --git a/lms/templates/static_templates/press_releases/UC_Berkeley_joins_edX.html b/lms/templates/static_templates/press_releases/uc_berkeley_joins_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/UC_Berkeley_joins_edX.html rename to lms/templates/static_templates/press_releases/uc_berkeley_joins_edx.html diff --git a/lms/templates/static_templates/press_releases/UT_joins_edX.html b/lms/templates/static_templates/press_releases/ut_joins_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/UT_joins_edX.html rename to lms/templates/static_templates/press_releases/ut_joins_edx.html diff --git a/lms/templates/static_templates/press_releases/Wellesley_College_joins_edX.html b/lms/templates/static_templates/press_releases/wellesley_college_joins_edx.html similarity index 100% rename from lms/templates/static_templates/press_releases/Wellesley_College_joins_edX.html rename to lms/templates/static_templates/press_releases/wellesley_college_joins_edx.html diff --git a/lms/templates/university_profile/epflx.html b/lms/templates/university_profile/epflx.html index 5119a223de..8153bd62b9 100644 --- a/lms/templates/university_profile/epflx.html +++ b/lms/templates/university_profile/epflx.html @@ -18,10 +18,13 @@ <%block name="university_description"> -

      EPFL is one of the two Swiss Federal Institutes of Technology. With the status of a national school since 1969, the young engineering school has grown in many dimensions, to the extent of becoming one of the most famous European institutions of science and technology. It has three core missions: training, research and technology transfer.

      -

      EPFL is located in Lausanne in Switzerland, on the shores of the largest lake in Europe, Lake Geneva and at the foot of the Alps and Mont-Blanc. Its main campus brings together over 11,000 persons, students, researchers and staff in the same magical place. Because of its dynamism and rich student community, EPFL has been able to create a special spirit imbued with curiosity and simplicity. Daily interactions amongst students, researchers and entrepreneurs on campus give rise to new scientific, technological and architectural projects. -

      +

      EPFL is the Swiss Federal Institute of Technology in Lausanne. The past decade has seen EPFL ascend to the very top of European institutions of science and technology: it is ranked #1 in Europe in the field of engineering by the Times Higher Education (based on publications and citations), Leiden Rankings, and the Academic Ranking of World Universities.

      + +

      EPFL's main campus brings together 12,600 students, faculty, researchers, and staff in a high-energy, dynamic learning and research environment. It directs the Human Brain Project, an undertaking to simulate the entire human brain using supercomputers, in order to gain new insights into how it operates and to better diagnose brain disorders. The university is building Solar Impulse, a long-range solar-powered plane that aims to be the first piloted fixed-wing aircraft to circumnavigate the Earth using only solar power. EPFL was part of the Alinghi project, developing advanced racing boats that won the America's Cup multiple times. The university operates, for education and research purposes, a Tokamak nuclear fusion reactor. EPFL also houses the Musée Bolo museum and hosts several music festivals, including Balelec, that draws over 15,000 guests every year.

      + +

      EPFL is a major force in entrepreneurship, with 2012 bringing in $100M in funding for ten EPFL startups. Both young spin-offs (like Typesafe and Pix4D) and companies that have long grown past the startup stage (like Logitech) actively transfer the results of EPFL's scientific innovation to industry.

      + ${parent.body()} diff --git a/lms/templates/university_profile/utaustinx.html b/lms/templates/university_profile/utaustinx.html new file mode 100644 index 0000000000..048587b334 --- /dev/null +++ b/lms/templates/university_profile/utaustinx.html @@ -0,0 +1,23 @@ +<%inherit file="base.html" /> +<%namespace name='static' file='../static_content.html'/> + +<%block name="title">UTAustinX + +<%block name="university_header"> + + + +<%block name="university_description"> +

      The University of Texas at Austin is the top-ranked public university in a nearly 1,000-mile radius, and is ranked in the top 25 universities in the world. Students have been finding their passion in life at UT Austin for more than 130 years, and it has been a member of the prestigious AAU since 1929. UT Austin combines the academic depth and breadth of a world research institute (regularly ranking within the top three producers of doctoral degrees in the country) with the fun and excitement of a big-time collegiate experience. It is currently the fifth-largest university in America, with more than 50,000 students and 3,000 professors across 17 colleges and schools. UT Austin will be opening the Dell Medical School in 2016.

      + + +${parent.body()} diff --git a/lms/templates/university_profile/utx.html b/lms/templates/university_profile/utx.html index b9378f6ce3..d25d8f09b8 100644 --- a/lms/templates/university_profile/utx.html +++ b/lms/templates/university_profile/utx.html @@ -1,5 +1,8 @@ <%inherit file="base.html" /> <%namespace name='static' file='../static_content.html'/> +<%! + from django.core.urlresolvers import reverse +%> <%block name="title">UTx @@ -19,6 +22,7 @@ <%block name="university_description">

      Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state’s undergraduate degrees and educates nearly three-fourths of the state’s health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state.

      +

      Find out about The University of Texas at Austin.

      ${parent.body()} diff --git a/lms/urls.py b/lms/urls.py index 16e6ba8165..01bb55b471 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -2,7 +2,6 @@ from django.conf import settings from django.conf.urls import patterns, include, url from django.contrib import admin from django.conf.urls.static import static -from django.views.generic import RedirectView from . import one_time_startup @@ -10,10 +9,9 @@ import django.contrib.auth.views # Uncomment the next two lines to enable the admin: if settings.DEBUG: - from django.contrib import admin admin.autodiscover() -urlpatterns = ('', +urlpatterns = ('', # nopep8 # certificate view url(r'^update_certificate$', 'certificates.views.update_certificate'), @@ -32,12 +30,6 @@ urlpatterns = ('', url(r'^accept_name_change$', 'student.views.accept_name_change'), url(r'^reject_name_change$', 'student.views.reject_name_change'), url(r'^pending_name_changes$', 'student.views.pending_name_changes'), - - url(r'^testcenter/login$', 'student.views.test_center_login'), - - # url(r'^testcenter/login$', 'student.test_center_views.login'), - # url(r'^testcenter/logout$', 'student.test_center_views.logout'), - url(r'^event$', 'track.views.user_track'), url(r'^t/(?P