diff --git a/.gitignore b/.gitignore index 2fd1ca0181..493df5a7fd 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ lms/lib/comment_client/python nosetests.xml cover_html/ .idea/ +.redcar/ chromedriver.log \ No newline at end of file diff --git a/.pep8 b/.pep8 new file mode 100644 index 0000000000..25d0edbcb4 --- /dev/null +++ b/.pep8 @@ -0,0 +1,2 @@ +[pep8] +ignore=E501 \ No newline at end of file diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..dd472cffa2 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +1.8.7-p371 \ No newline at end of file diff --git a/Gemfile b/Gemfile index 0fe7df217d..43a9f6e2b1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,6 @@ source :rubygems -ruby "1.9.3" -gem 'rake' +gem 'rake', '~> 10.0.3' gem 'sass', '3.1.15' gem 'bourbon', '~> 1.3.6' -gem 'colorize' -gem 'launchy' +gem 'colorize', '~> 0.5.8' +gem 'launchy', '~> 2.1.2' diff --git a/apt-packages.txt b/apt-packages.txt index b783ccb67e..0560dfcbc2 100644 --- a/apt-packages.txt +++ b/apt-packages.txt @@ -9,6 +9,7 @@ gfortran liblapack-dev libfreetype6-dev libpng12-dev +libjpeg-dev libxml2-dev libxslt-dev yui-compressor diff --git a/cms/.coveragerc b/cms/.coveragerc index 9b1e59d670..4f0dbebe79 100644 --- a/cms/.coveragerc +++ b/cms/.coveragerc @@ -1,8 +1,8 @@ # .coveragerc for cms [run] data_file = reports/cms/.coverage -source = cms -omit = cms/envs/*, cms/manage.py +source = cms,common/djangoapps +omit = cms/envs/*, cms/manage.py, common/djangoapps/terrain/*, common/djangoapps/*/migrations/* [report] ignore_errors = True diff --git a/cms/__init__.py b/cms/__init__.py index 8b13789179..e69de29bb2 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -1 +0,0 @@ - diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 22bbc4bc1c..281e3f46b2 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -18,6 +18,8 @@ STAFF_ROLE_NAME = 'staff' # we're just making a Django group for each location/role combo # to do this we're just creating a Group name which is a formatted string # of those two variables + + def get_course_groupname_for_role(location, role): loc = Location(location) # hack: check for existence of a group name in the legacy LMS format _ @@ -25,11 +27,12 @@ def get_course_groupname_for_role(location, role): # more information groupname = '{0}_{1}'.format(role, loc.course) - if len(Group.objects.filter(name = groupname)) == 0: - groupname = '{0}_{1}'.format(role,loc.course_id) + if len(Group.objects.filter(name=groupname)) == 0: + groupname = '{0}_{1}'.format(role, loc.course_id) return groupname + def get_users_in_course_group_by_role(location, role): groupname = get_course_groupname_for_role(location, role) (group, created) = Group.objects.get_or_create(name=groupname) @@ -39,6 +42,8 @@ def get_users_in_course_group_by_role(location, role): ''' Create all permission groups for a new course and subscribe the caller into those roles ''' + + def create_all_course_groups(creator, location): create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME) create_new_course_group(creator, location, STAFF_ROLE_NAME) @@ -46,7 +51,7 @@ def create_all_course_groups(creator, location): def create_new_course_group(creator, location, role): groupname = get_course_groupname_for_role(location, role) - (group, created) =Group.objects.get_or_create(name=groupname) + (group, created) = Group.objects.get_or_create(name=groupname) if created: group.save() @@ -59,6 +64,8 @@ def create_new_course_group(creator, location, role): This is to be called only by either a command line code path or through a app which has already asserted permissions ''' + + def _delete_course_group(location): # remove all memberships instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME)) @@ -75,6 +82,8 @@ def _delete_course_group(location): This is to be called only by either a command line code path or through an app which has already asserted permissions to do this action ''' + + def _copy_course_group(source, dest): instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME)) new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME)) @@ -86,7 +95,7 @@ def _copy_course_group(source, dest): new_staff_group = Group.objects.get(name=get_course_groupname_for_role(dest, STAFF_ROLE_NAME)) for user in staff.user_set.all(): user.groups.add(new_staff_group) - user.save() + user.save() def add_user_to_course_group(caller, user, location, role): @@ -133,8 +142,6 @@ def remove_user_from_course_group(caller, user, location, role): def is_user_in_course_group_role(user, location, role): if user.is_active and user.is_authenticated: # all "is_staff" flagged accounts belong to all groups - return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location,role)).count() > 0 + return user.is_staff or user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0 return False - - diff --git a/cms/djangoapps/contentstore/__init__.py b/cms/djangoapps/contentstore/__init__.py index e8dccbbf60..8b13789179 100644 --- a/cms/djangoapps/contentstore/__init__.py +++ b/cms/djangoapps/contentstore/__init__.py @@ -1,3 +1 @@ -from xmodule.templates import update_templates -update_templates() diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 6995df06a8..153d13dd13 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -8,6 +8,8 @@ import logging ## TODO store as array of { date, content } and override course_info_module.definition_from_xml ## This should be in a class which inherits from XmlDescriptor + + def get_course_updates(location): """ Retrieve the relevant course_info updates and unpack into the model which the client expects: @@ -21,13 +23,13 @@ def get_course_updates(location): # current db rep: {"_id" : locationjson, "definition" : { "data" : "
    [
  1. date

    content
  2. ]
"} "metadata" : ignored} location_base = course_updates.location.url() - + # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break. try: course_html_parsed = html.fromstring(course_updates.definition['data']) except: course_html_parsed = html.fromstring("
    ") - + # Confirm that root is
      , iterate over
    1. , pull out

      subs and then rest of val course_upd_collection = [] if course_html_parsed.tag == 'ol': @@ -40,25 +42,26 @@ def get_course_updates(location): content = update[0].tail else: content = "\n".join([html.tostring(ele) for ele in update[1:]]) - + # make the id on the client be 1..len w/ 1 being the oldest and len being the newest - course_upd_collection.append({"id" : location_base + "/" + str(len(course_html_parsed) - idx), - "date" : update.findtext("h2"), - "content" : content}) - + course_upd_collection.append({"id": location_base + "/" + str(len(course_html_parsed) - idx), + "date": update.findtext("h2"), + "content": content}) + return course_upd_collection + def update_course_updates(location, update, passed_id=None): """ Either add or update the given course update. It will add it if the passed_id is absent or None. It will update it if it has an passed_id which has a valid value. Until updates have distinct values, the passed_id is the location url + an index - into the html structure. + into the html structure. """ try: course_updates = modulestore('direct').get_item(location) except ItemNotFoundError: return HttpResponseBadRequest - + # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break. try: course_html_parsed = html.fromstring(course_updates.definition['data']) @@ -67,7 +70,7 @@ def update_course_updates(location, update, passed_id=None): # No try/catch b/c failure generates an error back to client new_html_parsed = html.fromstring('
    2. ' + update['date'] + '

      ' + update['content'] + '
    3. ') - + # Confirm that root is
        , iterate over
      1. , pull out

        subs and then rest of val if course_html_parsed.tag == 'ol': # ??? Should this use the id in the json or in the url or does it matter? @@ -80,14 +83,15 @@ def update_course_updates(location, update, passed_id=None): idx = len(course_html_parsed) passed_id = course_updates.location.url() + "/" + str(idx) - + # update db record course_updates.definition['data'] = html.tostring(course_html_parsed) modulestore('direct').update_item(location, course_updates.definition['data']) - - return {"id" : passed_id, - "date" : update['date'], - "content" :update['content']} + + return {"id": passed_id, + "date": update['date'], + "content": update['content']} + def delete_course_update(location, update, passed_id): """ @@ -96,19 +100,19 @@ def delete_course_update(location, update, passed_id): """ if not passed_id: return HttpResponseBadRequest - + try: course_updates = modulestore('direct').get_item(location) except ItemNotFoundError: return HttpResponseBadRequest - + # TODO use delete_blank_text parser throughout and cache as a static var in a class # purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break. try: course_html_parsed = html.fromstring(course_updates.definition['data']) except: course_html_parsed = html.fromstring("
          ") - + if course_html_parsed.tag == 'ol': # ??? Should this use the id in the json or in the url or does it matter? idx = get_idx(passed_id) @@ -120,10 +124,11 @@ def delete_course_update(location, update, passed_id): # update db record course_updates.definition['data'] = html.tostring(course_html_parsed) store = modulestore('direct') - store.update_item(location, course_updates.definition['data']) - + store.update_item(location, course_updates.definition['data']) + return get_course_updates(location) - + + def get_idx(passed_id): """ From the url w/ idx appended, get the idx. @@ -131,4 +136,4 @@ def get_idx(passed_id): # TODO compile this regex into a class static and reuse for each call idx_matcher = re.search(r'.*/(\d+)$', passed_id) if idx_matcher: - return int(idx_matcher.group(1)) \ No newline at end of file + return int(idx_matcher.group(1)) diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature new file mode 100644 index 0000000000..4708a60be1 --- /dev/null +++ b/cms/djangoapps/contentstore/features/advanced-settings.feature @@ -0,0 +1,51 @@ +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 + + Scenario: A course author sees only display_name on a newly created course + Given I have opened a new course in Studio + When I select the Advanced Settings + Then I see only the display name + + Scenario: Test if there are no policy settings without existing UI controls + Given I am on the Advanced Course Settings page in Studio + When I delete the display name + Then there are no advanced policy settings + And I reload the page + Then there are no advanced policy settings + + Scenario: Test cancel editing key name + Given I am on the Advanced Course Settings page in Studio + When I edit the name of a policy key + And I press the "Cancel" notification button + Then the policy key name is unchanged + + Scenario: Test editing key name + Given I am on the Advanced Course Settings page in Studio + When I edit the name of a policy key + And I press the "Save" notification button + Then the policy key name is changed + + 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 + And I press the "Cancel" notification button + Then the policy key value is unchanged + + 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 + Then the policy key value is changed + + Scenario: Add new entries, and they appear alphabetically after save + Given I am on the Advanced Course Settings page in Studio + When I create New Entries + Then they are alphabetized + And I reload the page + Then they are alphabetized + + Scenario: Test how multi-line input appears + Given I am on the Advanced Course Settings page in Studio + When I create a JSON object + Then it is displayed as formatted diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py new file mode 100644 index 0000000000..91daf70718 --- /dev/null +++ b/cms/djangoapps/contentstore/features/advanced-settings.py @@ -0,0 +1,182 @@ +from lettuce import world, step +from common import * +import time + +from nose.tools import assert_equal +from nose.tools import assert_true + +""" +http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html +""" +from selenium.webdriver.common.keys import Keys + + +############### 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) + link_css = 'li.nav-course-settings-advanced a' + css_click(link_css) + + +@step('I am on the Advanced Course Settings page in Studio$') +def i_am_on_advanced_course_settings(step): + step.given('I have opened a new course in Studio') + step.given('I select the Advanced Settings') + + +# TODO: this is copied from terrain's step.py. Need to figure out how to share that code. +@step('I reload the page$') +def reload_the_page(step): + world.browser.reload() + + +@step(u'I edit the name of a policy key$') +def edit_the_name_of_a_policy_key(step): + policy_key_css = 'input.policy-key' + e = css_find(policy_key_css).first + e.fill('new') + + +@step(u'I press the "([^"]*)" notification button$') +def press_the_notification_button(step, name): + world.browser.click_link_by_text(name) + + +@step(u'I edit the value of a policy key$') +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 :) + """ + policy_key_css = 'input.policy-key' + e = css_find(policy_key_css).first + e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X') + + +@step('I delete the display name$') +def delete_the_display_name(step): + delete_entry(0) + click_save() + + +@step('create New Entries$') +def create_new_entries(step): + create_entry("z", "apple") + create_entry("a", "zebra") + click_save() + + +@step('I create a JSON object$') +def create_JSON_object(step): + create_entry("json", '{"key": "value", "key_2": "value_2"}') + click_save() + + +############### RESULTS #################### +@step('I see only the display name$') +def i_see_only_display_name(step): + assert_policy_entries(["display_name"], ['"Robot Super Course"']) + + +@step('there are no advanced policy settings$') +def no_policy_settings(step): + assert_policy_entries([], []) + + +@step('they are alphabetized$') +def they_are_alphabetized(step): + assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"']) + + +@step('it is displayed as formatted$') +def it_is_formatted(step): + assert_policy_entries(["display_name", "json"], ['"Robot Super Course"', '{\n "key": "value",\n "key_2": "value_2"\n}']) + + +@step(u'the policy key name is unchanged$') +def the_policy_key_name_is_unchanged(step): + policy_key_css = 'input.policy-key' + e = css_find(policy_key_css).first + assert_equal(e.value, 'display_name') + + +@step(u'the policy key name is changed$') +def the_policy_key_name_is_changed(step): + policy_key_css = 'input.policy-key' + e = css_find(policy_key_css).first + assert_equal(e.value, 'new') + + +@step(u'the policy key value is unchanged$') +def the_policy_key_value_is_unchanged(step): + policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' + e = css_find(policy_value_css).first + assert_equal(e.value, '"Robot Super Course"') + + +@step(u'the policy key value is changed$') +def the_policy_key_value_is_unchanged(step): + policy_value_css = 'li.course-advanced-policy-list-item div.value textarea' + e = css_find(policy_value_css).first + assert_equal(e.value, '"Robot Super Course X"') + + +############# HELPERS ############### +def create_entry(key, value): + # Scroll down the page so the button is visible + world.scroll_to_bottom() + css_click_at('a.new-advanced-policy-item', 10, 10) + new_key_css = 'div#__new_advanced_key__ input' + new_key_element = css_find(new_key_css).first + new_key_element.fill(key) +# For some reason have to get the instance for each command (get error that it is no longer attached to the DOM) +# Have to do all this because Selenium has a bug that fill does not remove existing text + new_value_css = 'div.CodeMirror textarea' + css_find(new_value_css).last.fill("") + css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE) + css_find(new_value_css).last.fill(value) + + +def delete_entry(index): + """ + Delete the nth entry where index is 0-based + """ + css = '.delete-button' + assert_true(world.browser.is_element_present_by_css(css, 5)) + delete_buttons = css_find(css) + assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index)) + delete_buttons[index].click() + + +def assert_policy_entries(expected_keys, expected_values): + assert_entries('.key input', expected_keys) + assert_entries('.json', expected_values) + + +def assert_entries(css, expected_values): + webElements = css_find(css) + assert_equal(len(expected_values), len(webElements)) +# Sometimes get stale reference if I hold on to the array of elements + for counter in range(len(expected_values)): + assert_equal(expected_values[counter], css_find(css)[counter].value) + + +def click_save(): + css = ".save-button" + + def is_shown(driver): + visible = css_find(css).first.visible + if visible: + # Even when waiting for visible, this fails sporadically. Adding in a small wait. + time.sleep(float(1)) + return visible + wait_for(is_shown) + css_click(css) + + +def fill_last_field(value): + newValue = css_find('#__new_advanced_key__ input').first + newValue.fill(value) diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index d910d73085..61b4fee9f6 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -1,51 +1,67 @@ from lettuce import world, step -from factories import * -from django.core.management import call_command from lettuce.django import django_url -from django.conf import settings -from django.core.management import call_command from nose.tools import assert_true from nose.tools import assert_equal +from selenium.webdriver.support.ui import WebDriverWait + +from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory +from terrain.factories import CourseFactory, GroupFactory import xmodule.modulestore.django +from auth.authz import get_user_by_email 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('/')) - assert world.browser.is_element_present_by_css('body.no-header', 10) + signin_css = 'a.action-signin' + assert world.browser.is_element_present_by_css(signin_css, 10) + @step('I am logged into Studio$') def i_am_logged_into_studio(step): log_into_studio() + @step('I confirm the alert$') def i_confirm_with_ok(step): world.browser.get_alert().accept() + @step(u'I press the "([^"]*)" delete icon$') def i_press_the_category_delete_icon(step, category): if category == 'section': css = 'a.delete-button.delete-section-button span.delete-icon' elif category == 'subsection': - css='a.delete-button.delete-subsection-button span.delete-icon' + css = 'a.delete-button.delete-subsection-button span.delete-icon' else: assert False, 'Invalid category: %s' % category css_click(css) + +@step('I have opened a new course in Studio$') +def i_have_opened_a_new_course(step): + clear_courses() + log_into_studio() + create_a_course() + ####### HELPER FUNCTIONS ############## + + def create_studio_user( uname='robot', email='robot+studio@edx.org', password='test', - is_staff=False): + is_staff=False): studio_user = UserFactory.build( - username=uname, + username=uname, email=email, password=password, is_staff=is_staff) @@ -58,6 +74,7 @@ def create_studio_user( user_profile = UserProfileFactory(user=studio_user) + def flush_xmodule_store(): # Flush and initialize the module store # It needs the templates because it creates new records @@ -70,26 +87,57 @@ def flush_xmodule_store(): xmodule.modulestore.django.modulestore().collection.drop() xmodule.templates.update_templates() -def assert_css_with_text(css,text): + +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): + assert_true(world.browser.is_element_present_by_css(css, 5)) world.browser.find_by_css(css).first.click() + +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 + ''' + assert_true(world.browser.is_element_present_by_css(css, 5)) + e = world.browser.find_by_css(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): + return world.browser.find_by_css(css) + + +def wait_for(func): + WebDriverWait(world.browser.driver, 10).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) + css_fill('.new-course-name', name) + css_fill('.new-course-org', org) + css_fill('.new-course-number', num) + def log_into_studio( uname='robot', @@ -99,7 +147,11 @@ def log_into_studio( create_studio_user(uname=uname, email=email, is_staff=is_staff) world.browser.cookies.delete() world.browser.visit(django_url('/')) - world.browser.is_element_present_by_css('body.no-header', 10) + signin_css = 'a.action-signin' + world.browser.is_element_present_by_css(signin_css, 10) + + # click the signin button + css_click(signin_css) login_form = world.browser.find_by_css('form#login_form') login_form.find_by_name('email').fill(email) @@ -108,19 +160,34 @@ def log_into_studio( assert_true(world.browser.is_element_present_by_css('.new-course-button', 5)) + def create_a_course(): - css_click('a.new-course-button') - fill_in_course_info() - css_click('input.new-course-save') - assert_true(world.browser.is_element_present_by_css('a#courseware-tab', 5)) + c = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') + + # Add the user to the instructor group of the course + # so they will have the permissions to see it in studio + g = GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course') + u = get_user_by_email('robot+studio@edx.org') + u.groups.add(g) + u.save() + world.browser.reload() + + course_link_css = 'span.class-name' + css_click(course_link_css) + course_title_css = 'span.course-title' + assert_true(world.browser.is_element_present_by_css(course_title_css, 5)) + def add_section(name='My Section'): link_css = 'a.new-courseware-section-button' css_click(link_css) - name_css = '.new-section-name' - save_css = '.new-section-name-save' - css_fill(name_css,name) + name_css = 'input.new-section-name' + save_css = 'input.new-section-name-save' + css_fill(name_css, name) css_click(save_css) + span_css = 'span.section-name-span' + assert_true(world.browser.is_element_present_by_css(span_css, 5)) + def add_subsection(name='Subsection One'): css = 'a.new-subsection-item' @@ -128,4 +195,4 @@ def add_subsection(name='Subsection One'): name_css = 'input.new-subsection-name-input' save_css = 'input.new-subsection-name-save' css_fill(name_css, name) - css_click(save_css) \ No newline at end of file + css_click(save_css) diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index 2c1cf6281a..e394165f08 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -2,49 +2,61 @@ from lettuce import world, step from common import * ############### ACTIONS #################### + + @step('There are no courses$') def no_courses(step): clear_courses() + @step('I click the New Course button$') def i_click_new_course(step): css_click('.new-course-button') + @step('I fill in the new course information$') def i_fill_in_a_new_course_information(step): fill_in_course_info() + @step('I create a new course$') def i_create_a_course(step): create_a_course() + @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) ############ ASSERTIONS ################### + + @step('the Courseware page has loaded in Studio$') def courseware_page_has_loaded_in_studio(step): - courseware_css = 'a#courseware-tab' - assert world.browser.is_element_present_by_css(courseware_css) + course_title_css = 'span.course-title' + assert world.browser.is_element_present_by_css(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_css_with_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_css_with_text(class_css, 'Robot Super Course') + @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_css_with_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_css_with_text(link_css, '+ New Section') diff --git a/cms/djangoapps/contentstore/features/factories.py b/cms/djangoapps/contentstore/features/factories.py index 389f2bac49..087ceaaa2d 100644 --- a/cms/djangoapps/contentstore/features/factories.py +++ b/cms/djangoapps/contentstore/features/factories.py @@ -3,6 +3,7 @@ from student.models import User, UserProfile, Registration from datetime import datetime import uuid + class UserProfileFactory(factory.Factory): FACTORY_FOR = UserProfile @@ -10,12 +11,14 @@ class UserProfileFactory(factory.Factory): name = 'Robot Studio' courseware = 'course.xml' + class RegistrationFactory(factory.Factory): FACTORY_FOR = Registration user = None activation_key = uuid.uuid4().hex + class UserFactory(factory.Factory): FACTORY_FOR = User @@ -28,4 +31,4 @@ class UserFactory(factory.Factory): is_active = True is_superuser = False last_login = datetime.now() - date_joined = datetime.now() \ No newline at end of file + date_joined = datetime.now() diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index ad00ba2911..75e7a4af10 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -11,6 +11,14 @@ Feature: Create Section And I see a release date for my section And I see a link to create a new subsection + Scenario: Add a new section (with a quote in the name) to a course (bug #216) + Given I have opened a new course in Studio + When I click the New Section link + And I enter a section name with a quote and click save + Then I see my section name with a quote on the Courseware page + And I click to edit the section name + Then I see the complete section name with a quote in the editor + Scenario: Edit section release date Given I have opened a new course in Studio And I have added a new section diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index 8ac30e2170..cfa4e4bb52 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -1,55 +1,79 @@ from lettuce import world, step from common import * +from nose.tools import assert_equal ############### ACTIONS #################### -@step('I have opened a new course in Studio$') -def i_have_opened_a_new_course(step): - clear_courses() - log_into_studio() - create_a_course() + @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) + @step('I enter the section name and click save$') def i_save_section_name(step): - name_css = '.new-section-name' - save_css = '.new-section-name-save' - css_fill(name_css,'My Section') - css_click(save_css) + save_section_name('My Section') + + +@step('I enter a section name with a quote and click save$') +def i_save_section_name_with_quote(step): + save_section_name('Section with "Quote"') + @step('I have added a new section$') def i_have_added_new_section(step): add_section() - + + @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) + @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') + css_fill(date_css, '12/25/2013') # click here to make the calendar go away css_click(time_css) - css_fill(time_css,'12:00am') + css_fill(time_css, '12:00am') css_click('a.save-button') + ############ ASSERTIONS ################### + + @step('I see my section on the Courseware page$') def i_see_my_section_on_the_courseware_page(step): - section_css = 'span.section-name-span' - assert_css_with_text(section_css,'My Section') + see_my_section_on_the_courseware_page('My Section') + + +@step('I see my section name with a quote on the Courseware page$') +def i_see_my_section_name_with_quote_on_the_courseware_page(step): + see_my_section_on_the_courseware_page('Section with "Quote"') + + +@step('I click to edit the section name$') +def i_click_to_edit_section_name(step): + 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_equal(world.browser.find_by_css(css).value, 'Section with "Quote"') + @step('the section does not exist$') def section_does_not_exist(step): css = 'span.section-name-span' assert world.browser.is_element_not_present_by_css(css) + @step('I see a release date for my section$') def i_see_a_release_date_for_my_section(step): import re @@ -63,20 +87,37 @@ def i_see_a_release_date_for_my_section(step): date_regex = '[01][0-9]\/[0-3][0-9]\/[12][0-9][0-9][0-9]' time_regex = '[0-2][0-9]:[0-5][0-9]' match_string = '%s %s at %s' % (msg, date_regex, time_regex) - assert re.match(match_string,status_text) + assert re.match(match_string, status_text) + @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) + @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 + @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 status_text == 'Will Release: 12/25/2013 at 12:00am' + + +############ HELPER METHODS ################### + +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) + + +def see_my_section_on_the_courseware_page(name): + section_css = 'span.section-name-span' + assert_css_with_text(section_css, name) \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/signup.feature b/cms/djangoapps/contentstore/features/signup.feature index 8a6f93d33b..03a1c9524a 100644 --- a/cms/djangoapps/contentstore/features/signup.feature +++ b/cms/djangoapps/contentstore/features/signup.feature @@ -5,8 +5,8 @@ Feature: Sign in Scenario: Sign up from the homepage Given I visit the Studio homepage - When I click the link with the text "Sign up" + When I click the link with the text "Sign Up" And I fill in the registration form - And I press the "Create My Account" button on the registration form + And I press the Create My Account button on the registration form Then I should see be on the studio home page - And I should see the message "please click on the activation link in your email." \ No newline at end of file + And I should see the message "please click on the activation link in your email." diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index 7794511f94..a786225ead 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -1,5 +1,6 @@ from lettuce import world, step + @step('I fill in the registration form$') def i_fill_in_the_registration_form(step): register_form = world.browser.find_by_css('form#register_form') @@ -9,15 +10,19 @@ def i_fill_in_the_registration_form(step): register_form.find_by_name('name').fill('Robot Studio') register_form.find_by_name('terms_of_service').check() -@step('I press the "([^"]*)" button on the registration form$') -def i_press_the_button_on_the_registration_form(step, button): + +@step('I press the Create My Account button on the registration form$') +def i_press_the_button_on_the_registration_form(step): register_form = world.browser.find_by_css('form#register_form') - register_form.find_by_value(button).click() + submit_css = 'button#submit' + register_form.find_by_css(submit_css).click() + @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') + @step(u'I should see the message "([^"]*)"$') def i_should_see_the_message(step, msg): - assert world.browser.is_text_present(msg, 5) \ No newline at end of file + assert world.browser.is_text_present(msg, 5) diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py index 010678c0e8..00aa39455d 100644 --- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py +++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py @@ -6,11 +6,13 @@ from nose.tools import assert_true, assert_false, assert_equal from logging import getLogger logger = getLogger(__name__) + @step(u'I have a course with no sections$') def have_a_course(step): clear_courses() course = CourseFactory.create() + @step(u'I have a course with 1 section$') def have_a_course_with_1_section(step): clear_courses() @@ -18,8 +20,9 @@ def have_a_course_with_1_section(step): section = ItemFactory.create(parent_location=course.location) subsection1 = ItemFactory.create( parent_location=section.location, - template = 'i4x://edx/templates/sequential/Empty', - display_name = 'Subsection One',) + template='i4x://edx/templates/sequential/Empty', + display_name='Subsection One',) + @step(u'I have a course with multiple sections$') def have_a_course_with_two_sections(step): @@ -28,19 +31,20 @@ def have_a_course_with_two_sections(step): section = ItemFactory.create(parent_location=course.location) subsection1 = ItemFactory.create( parent_location=section.location, - template = 'i4x://edx/templates/sequential/Empty', - display_name = 'Subsection One',) + template='i4x://edx/templates/sequential/Empty', + display_name='Subsection One',) section2 = ItemFactory.create( parent_location=course.location, display_name='Section Two',) subsection2 = ItemFactory.create( parent_location=section2.location, - template = 'i4x://edx/templates/sequential/Empty', - display_name = 'Subsection Alpha',) + template='i4x://edx/templates/sequential/Empty', + display_name='Subsection Alpha',) subsection3 = ItemFactory.create( parent_location=section2.location, - template = 'i4x://edx/templates/sequential/Empty', - display_name = 'Subsection Beta',) + template='i4x://edx/templates/sequential/Empty', + display_name='Subsection Beta',) + @step(u'I navigate to the course overview page$') def navigate_to_the_course_overview_page(step): @@ -48,15 +52,18 @@ def navigate_to_the_course_overview_page(step): course_locator = '.class-name' css_click(course_locator) + @step(u'I navigate to the courseware page of a course with multiple sections') def nav_to_the_courseware_page_of_a_course_with_multiple_sections(step): step.given('I have a course with multiple sections') step.given('I navigate to the course overview page') + @step(u'I add a section') def i_add_a_section(step): add_section(name='My New Section That I Just Added') + @step(u'I click the "([^"]*)" link$') def i_click_the_text_span(step, text): span_locator = '.toggle-button-sections span' @@ -65,16 +72,19 @@ def i_click_the_text_span(step, text): assert_equal(world.browser.find_by_css(span_locator).value, text) 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) + @step(u'I expand the first section$') def i_expand_a_section(step): expand_locator = 'section.courseware-section a.expand' 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' @@ -82,6 +92,7 @@ def i_see_the_span_with_text(step, text): assert_equal(world.browser.find_by_css(span_locator).value, text) assert_true(world.browser.find_by_css(span_locator).visible) + @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 @@ -89,6 +100,7 @@ def i_do_not_see_the_span_with_text(step, text): assert_true(world.browser.is_element_present_by_css(span_locator)) assert_false(world.browser.find_by_css(span_locator).visible) + @step(u'all sections are expanded$') def all_sections_are_expanded(step): subsection_locator = 'div.subsection-list' @@ -96,9 +108,10 @@ def all_sections_are_expanded(step): for s in subsections: assert_true(s.visible) + @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) for s in subsections: - assert_false(s.visible) \ No newline at end of file + assert_false(s.visible) diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 5acb5bfe44..4b5f5b869d 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -9,6 +9,14 @@ Feature: Create Subsection 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) + 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 + Then I see my subsection name with a quote on the Courseware page + And I click to edit the subsection name + Then I see the complete subsection name with a quote in the editor + Scenario: Delete a subsection Given I have opened a new course section in Studio And I have added a new subsection diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index ea614d3feb..88e1424898 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -1,7 +1,10 @@ from lettuce import world, step from common import * +from nose.tools import assert_equal ############### ACTIONS #################### + + @step('I have opened a new course section in Studio$') def i_have_opened_a_new_course_section(step): clear_courses() @@ -9,31 +12,69 @@ def i_have_opened_a_new_course_section(step): create_a_course() add_section() + @step('I click the New Subsection link') def i_click_the_new_subsection_link(step): css = 'a.new-subsection-item' css_click(css) + @step('I enter the subsection name and click save$') def i_save_subsection_name(step): - name_css = 'input.new-subsection-name-input' - save_css = 'input.new-subsection-name-save' - css_fill(name_css,'Subsection One') - css_click(save_css) + save_subsection_name('Subsection One') + + +@step('I enter a subsection name with a quote and click save$') +def i_save_subsection_name_with_quote(step): + save_subsection_name('Subsection With "Quote"') + + +@step('I click to edit the subsection name$') +def i_click_to_edit_subsection_name(step): + 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"') + @step('I have added a new subsection$') def i_have_added_a_new_subsection(step): add_subsection() + ############ ASSERTIONS ################### + + @step('I see my subsection on the Courseware page$') def i_see_my_subsection_on_the_courseware_page(step): - css = 'span.subsection-name' - assert world.browser.is_element_present_by_css(css) - css = 'span.subsection-name-value' - assert_css_with_text(css,'Subsection One') + see_subsection_name('Subsection One') + + +@step('I see my subsection name with a quote on the Courseware page$') +def i_see_my_subsection_name_with_quote_on_the_courseware_page(step): + see_subsection_name('Subsection With "Quote"') + @step('the subsection does not exist$') def the_subsection_does_not_exist(step): css = 'span.subsection-name' - assert world.browser.is_element_not_present_by_css(css) \ No newline at end of file + assert world.browser.is_element_not_present_by_css(css) + + +############ HELPER METHODS ################### + +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) + +def see_subsection_name(name): + css = 'span.subsection-name' + assert world.browser.is_element_present_by_css(css) + css = 'span.subsection-name-value' + assert_css_with_text(css, name) diff --git a/cms/djangoapps/contentstore/management/commands/clone.py b/cms/djangoapps/contentstore/management/commands/clone.py index 2357cd1dbd..abf04f3da3 100644 --- a/cms/djangoapps/contentstore/management/commands/clone.py +++ b/cms/djangoapps/contentstore/management/commands/clone.py @@ -14,6 +14,7 @@ from auth.authz import _copy_course_group # To run from command line: rake cms:clone SOURCE_LOC=MITx/111/Foo1 DEST_LOC=MITx/135/Foo3 # + class Command(BaseCommand): help = \ '''Clone a MongoDB backed course to another location''' diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py index 0313f7faed..789226db1a 100644 --- a/cms/djangoapps/contentstore/management/commands/delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/delete_course.py @@ -15,26 +15,31 @@ from auth.authz import _delete_course_group # To run from command line: rake cms:delete_course LOC=MITx/111/Foo1 # + class Command(BaseCommand): - help = \ -'''Delete a MongoDB backed course''' + help = '''Delete a MongoDB backed course''' def handle(self, *args, **options): - if len(args) != 1: - raise CommandError("delete_course requires one argument: ") + if len(args) != 1 and len(args) != 2: + raise CommandError("delete_course requires one or more arguments: |commit|") loc_str = args[0] + commit = False + if len(args) == 2: + commit = args[1] == 'commit' + + if commit: + print 'Actually going to delete the course from DB....' + ms = modulestore('direct') cs = contentstore() 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) == True: - print 'removing User permissions from course....' - # in the django layer, we need to remove all the user permissions groups associated with this course - _delete_course_group(loc) - - - + 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: + 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: + _delete_course_group(loc) diff --git a/cms/djangoapps/contentstore/management/commands/prompt.py b/cms/djangoapps/contentstore/management/commands/prompt.py index 9c8fd81d45..40a39d0a11 100644 --- a/cms/djangoapps/contentstore/management/commands/prompt.py +++ b/cms/djangoapps/contentstore/management/commands/prompt.py @@ -1,5 +1,6 @@ import sys + def query_yes_no(question, default="yes"): """Ask a yes/no question via raw_input() and return their answer. @@ -12,7 +13,7 @@ def query_yes_no(question, default="yes"): """ valid = {"yes":True, "y":True, "ye":True, "no":False, "n":False} - if default == None: + if default is None: prompt = " [y/n] " elif default == "yes": prompt = " [Y/n] " @@ -30,4 +31,4 @@ def query_yes_no(question, default="yes"): return valid[choice] else: sys.stdout.write("Please respond with 'yes' or 'no' "\ - "(or 'y' or 'n').\n") \ No newline at end of file + "(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 new file mode 100644 index 0000000000..b30d30480a --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/update_templates.py @@ -0,0 +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''' + + def handle(self, *args, **options): + update_templates() \ No newline at end of file diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py index 0017010885..7ed4505c94 100644 --- a/cms/djangoapps/contentstore/module_info_model.py +++ b/cms/djangoapps/contentstore/module_info_model.py @@ -1,84 +1,92 @@ -import logging -from static_replace import replace_urls +from static_replace import replace_static_urls from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore import Location -from xmodule.modulestore.django import modulestore -from lxml import etree -import re -from django.http import HttpResponseBadRequest, Http404 +from django.http import Http404 -def get_module_info(store, location, parent_location = None, rewrite_static_links = False): - try: - if location.revision is None: - module = store.get_item(location) - else: - module = store.get_item(location) - except ItemNotFoundError: - raise Http404 - data = module.definition['data'] - if rewrite_static_links: - data = replace_urls(module.definition['data'], course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None])) +def get_module_info(store, location, parent_location=None, rewrite_static_links=False): + try: + if location.revision is None: + module = store.get_item(location) + else: + module = store.get_item(location) + except ItemNotFoundError: + # create a new one + template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) + module = store.clone_item(template_location, location) - return { + data = module.definition['data'] + if rewrite_static_links: + data = replace_static_urls( + module.definition['data'], + None, + course_namespace=Location([ + module.location.tag, + module.location.org, + module.location.course, + None, + None + ]) + ) + + return { 'id': module.location.url(), 'data': data, 'metadata': module.metadata } + def set_module_info(store, location, post_data): - module = None - isNew = False - try: - if location.revision is None: - module = store.get_item(location) - else: - module = store.get_item(location) - except: - pass + module = None + try: + if location.revision is None: + module = store.get_item(location) + else: + module = store.get_item(location) + except: + pass - if module is None: - # new module at this location - # presume that we have an 'Empty' template - template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) - module = store.clone_item(template_location, location) - isNew = True + if module is None: + # new module at this location + # presume that we have an 'Empty' template + template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) + module = store.clone_item(template_location, location) - if post_data.get('data') is not None: - data = post_data['data'] - store.update_item(location, data) - - # cdodge: note calling request.POST.get('children') will return None if children is an empty array - # so it lead to a bug whereby the last component to be deleted in the UI was not actually - # deleting the children object from the children collection - if 'children' in post_data and post_data['children'] is not None: - children = post_data['children'] - store.update_children(location, children) + if post_data.get('data') is not None: + data = post_data['data'] + store.update_item(location, data) - # cdodge: also commit any metadata which might have been passed along in the - # POST from the client, if it is there - # NOTE, that the postback is not the complete metadata, as there's system metadata which is - # not presented to the end-user for editing. So let's fetch the original and - # 'apply' the submitted metadata, so we don't end up deleting system metadata - if post_data.get('metadata') is not None: - posted_metadata = post_data['metadata'] + # cdodge: note calling request.POST.get('children') will return None if children is an empty array + # so it lead to a bug whereby the last component to be deleted in the UI was not actually + # deleting the children object from the children collection + if 'children' in post_data and post_data['children'] is not None: + children = post_data['children'] + store.update_children(location, children) - # update existing metadata with submitted metadata (which can be partial) - # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' - for metadata_key in posted_metadata.keys(): - - # let's strip out any metadata fields from the postback which have been identified as system metadata - # and therefore should not be user-editable, so we should accept them back from the client - if metadata_key in module.system_metadata_fields: - del posted_metadata[metadata_key] - elif posted_metadata[metadata_key] is None: - # remove both from passed in collection as well as the collection read in from the modulestore - if metadata_key in module.metadata: - del module.metadata[metadata_key] - del posted_metadata[metadata_key] - - # overlay the new metadata over the modulestore sourced collection to support partial updates - module.metadata.update(posted_metadata) - - # commit to datastore - store.update_metadata(location, module.metadata) + # cdodge: also commit any metadata which might have been passed along in the + # POST from the client, if it is there + # NOTE, that the postback is not the complete metadata, as there's system metadata which is + # not presented to the end-user for editing. So let's fetch the original and + # 'apply' the submitted metadata, so we don't end up deleting system metadata + if post_data.get('metadata') is not None: + posted_metadata = post_data['metadata'] + + # update existing metadata with submitted metadata (which can be partial) + # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' + for metadata_key in posted_metadata.keys(): + + # let's strip out any metadata fields from the postback which have been identified as system metadata + # and therefore should not be user-editable, so we should accept them back from the client + if metadata_key in module.system_metadata_fields: + del posted_metadata[metadata_key] + elif posted_metadata[metadata_key] is None: + # remove both from passed in collection as well as the collection read in from the modulestore + if metadata_key in module.metadata: + del module.metadata[metadata_key] + del posted_metadata[metadata_key] + + # overlay the new metadata over the modulestore sourced collection to support partial updates + module.metadata.update(posted_metadata) + + # commit to datastore + store.update_metadata(location, module.metadata) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py new file mode 100644 index 0000000000..8e4a016a0f --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -0,0 +1,512 @@ +import json +import shutil +from django.test.client import Client +from django.test.utils import override_settings +from django.conf import settings +from django.core.urlresolvers import reverse +from path import path +from tempdir import mkdtemp_clean +import json +from fs.osfs import OSFS +import copy +from mock import Mock +from json import dumps, loads + +from student.models import Registration +from django.contrib.auth.models import User +from cms.djangoapps.contentstore.utils import get_modulestore + +from utils import ModuleStoreTestCase, parse_json +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + +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.templates import update_templates + +from xmodule.capa_module import CapaDescriptor +from xmodule.course_module import CourseDescriptor +from xmodule.seq_module import SequenceDescriptor +from xmodule.modulestore.exceptions import ItemNotFoundError + +TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) +TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') +TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') + + +@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) +class ContentStoreToyCourseTest(ModuleStoreTestCase): + """ + Tests that rely on the toy courses. + TODO: refactor using CourseFactory so they do not. + """ + def setUp(self): + uname = 'testuser' + email = 'test+courses@edx.org' + password = 'foo' + + # Create the use so we can log them in. + self.user = User.objects.create_user(uname, email, 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.client = Client() + self.client.login(username=uname, password=password) + + + def check_edit_unit(self, test_course_name): + import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) + + for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)): + print "Checking ", descriptor.location.url() + print descriptor.__class__, descriptor.location + resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) + self.assertEqual(resp.status_code, 200) + + def test_edit_unit_toy(self): + self.check_edit_unit('toy') + + def test_edit_unit_full(self): + self.check_edit_unit('full') + + def test_static_tab_reordering(self): + import_from_xml(modulestore(), 'common/test/data/', ['full']) + + ms = modulestore('direct') + course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) + + # reverse the ordering + reverse_tabs = [] + for tab in course.tabs: + if tab['type'] == 'static_tab': + reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug'])) + + resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json") + + course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) + + # compare to make sure that the tabs information is in the expected order after the server call + course_tabs = [] + for tab in course.tabs: + if tab['type'] == 'static_tab': + course_tabs.append('i4x://edX/full/static_tab/{0}'.format(tab['url_slug'])) + + self.assertEqual(reverse_tabs, course_tabs) + + 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 + while there is a base definition in /about/effort.html + ''' + import_from_xml(modulestore(), 'common/test/data/', ['full']) + ms = modulestore('direct') + effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None])) + self.assertEqual(effort.definition['data'], '6 hours') + + # this one should be in a non-override folder + effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None])) + self.assertEqual(effort.definition['data'], 'TBD') + + def test_remove_hide_progress_tab(self): + import_from_xml(modulestore(), 'common/test/data/', ['full']) + + ms = modulestore('direct') + cs = contentstore() + + source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') + course = ms.get_item(source_location) + self.assertNotIn('hide_progress_tab', course.metadata) + + def test_clone_course(self): + + course_data = { + 'template': 'i4x://edx/templates/course/Empty', + 'org': 'MITx', + 'number': '999', + 'display_name': 'Robot Super Course', + } + + import_from_xml(modulestore(), 'common/test/data/', ['full']) + + resp = self.client.post(reverse('create_new_course'), course_data) + self.assertEqual(resp.status_code, 200) + data = parse_json(resp) + self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') + + ms = modulestore('direct') + cs = contentstore() + + source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') + dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course') + + clone_course(ms, cs, source_location, dest_location) + + # now loop through all the units in the course and verify that the clone can render them, which + # means the objects are at least present + items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) + self.assertGreater(len(items), 0) + clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None])) + self.assertGreater(len(clone_items), 0) + for descriptor in items: + new_loc = descriptor.location._replace(org='MITx', course='999') + print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url()) + resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()})) + self.assertEqual(resp.status_code, 200) + + def test_delete_course(self): + import_from_xml(modulestore(), 'common/test/data/', ['full']) + + ms = modulestore('direct') + cs = contentstore() + + location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') + + delete_course(ms, cs, location, commit=True) + + items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) + self.assertEqual(len(items), 0) + + def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''): + fs = OSFS(root_dir / 'test_export') + self.assertTrue(fs.exists(dirname)) + + query_loc = Location('i4x', location.org, location.course, category_name, None) + items = modulestore.get_items(query_loc) + + for item in items: + fs = OSFS(root_dir / ('test_export/' + dirname)) + self.assertTrue(fs.exists(item.location.name + filename_suffix)) + + def test_export_course(self): + ms = modulestore('direct') + cs = contentstore() + + import_from_xml(ms, 'common/test/data/', ['full']) + location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') + + root_dir = path(mkdtemp_clean()) + + print 'Exporting to tempdir = {0}'.format(root_dir) + + # export out to a tempdir + export_to_xml(ms, cs, location, root_dir, 'test_export') + + # check for static tabs + self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html') + + # check for custom_tags + self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html') + + # check for custom_tags + self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template') + + # check for graiding_policy.json + fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') + self.assertTrue(fs.exists('grading_policy.json')) + + course = ms.get_item(location) + # compare what's on disk compared to what we have in our course + with fs.open('grading_policy.json','r') as grading_policy: + on_disk = loads(grading_policy.read()) + self.assertEqual(on_disk, course.definition['data']['grading_policy']) + + #check for policy.json + self.assertTrue(fs.exists('policy.json')) + + # compare what's on disk to what we have in the course module + with fs.open('policy.json','r') as course_policy: + on_disk = loads(course_policy.read()) + self.assertIn('course/6.002_Spring_2012', on_disk) + self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata) + + # remove old course + delete_course(ms, cs, location) + + # reimport + import_from_xml(ms, root_dir, ['test_export']) + + items = ms.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) + + shutil.rmtree(root_dir) + + def test_course_handouts_rewrites(self): + ms = modulestore('direct') + cs = contentstore() + + # import a test course + import_from_xml(ms, 'common/test/data/', ['full']) + + handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts']) + + # get module info + resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location})) + + # make sure we got a successful response + self.assertEqual(resp.status_code, 200) + + # check that /static/ has been converted to the full path + # 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') + + + +class ContentStoreTest(ModuleStoreTestCase): + """ + Tests for the CMS ContentStore application. + """ + 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. + """ + uname = 'testuser' + email = 'test+courses@edx.org' + password = 'foo' + + # Create the use so we can log them in. + self.user = User.objects.create_user(uname, email, 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.client = Client() + self.client.login(username=uname, password=password) + + self.course_data = { + 'template': 'i4x://edx/templates/course/Empty', + 'org': 'MITx', + 'number': '999', + 'display_name': 'Robot Super Course', + } + + def test_create_course(self): + """Test new course creation - happy path""" + resp = self.client.post(reverse('create_new_course'), self.course_data) + self.assertEqual(resp.status_code, 200) + data = parse_json(resp) + self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') + + def test_create_course_duplicate_course(self): + """Test new course creation - error path""" + resp = self.client.post(reverse('create_new_course'), self.course_data) + resp = self.client.post(reverse('create_new_course'), self.course_data) + data = parse_json(resp) + self.assertEqual(resp.status_code, 200) + self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.') + + def test_create_course_duplicate_number(self): + """Test new course creation - error path""" + resp = self.client.post(reverse('create_new_course'), self.course_data) + self.course_data['display_name'] = 'Robot Super Course Two' + + resp = self.client.post(reverse('create_new_course'), self.course_data) + data = parse_json(resp) + + self.assertEqual(resp.status_code, 200) + self.assertEqual(data['ErrMsg'], + '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""" + self.course_data['org'] = 'University of California, Berkeley' + resp = self.client.post(reverse('create_new_course'), self.course_data) + data = parse_json(resp) + + 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'.") + + 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, + '

          My Courses

          ', + status_code=200, + html=True) + + def test_course_factory(self): + """Test that the course factory works correctly.""" + course = CourseFactory.create() + self.assertIsInstance(course, CourseDescriptor) + + def test_item_factory(self): + """Test that the item factory works correctly.""" + course = CourseFactory.create() + item = ItemFactory.create(parent_location=course.location) + self.assertIsInstance(item, SequenceDescriptor) + + def test_course_index_view_with_course(self): + """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, + 'Robot Super Educational Course', + status_code=200, + 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'), + } + + resp = self.client.get(reverse('course_index', kwargs=data)) + self.assertContains(resp, + '
          ', + status_code=200, + html=True) + + def test_clone_item(self): + """Test cloning an item. E.g. creating a new section""" + CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') + + section_data = { + '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}$') + + def test_capa_module(self): + """Test that a problem treats markdown specially.""" + course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') + + 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) + + self.assertEqual(resp.status_code, 200) + payload = parse_json(resp) + problem_loc = payload['id'] + problem = get_modulestore(problem_loc).get_item(problem_loc) + # should be a CapaDescriptor + self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor") + context = problem.get_context() + self.assertIn('markdown', context, "markdown is missing from context") + self.assertIn('markdown', problem.metadata, "markdown is missing from metadata") + self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields") + + def test_import_metadata_with_attempts_empty_string(self): + import_from_xml(modulestore(), 'common/test/data/', ['simple']) + ms = modulestore('direct') + did_load_item = False + try: + ms.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])) + did_load_item = True + except ItemNotFoundError: + pass + + # make sure we found the item (e.g. it didn't error while loading) + self.assertTrue(did_load_item) + + def test_metadata_inheritance(self): + import_from_xml(modulestore(), 'common/test/data/', ['full']) + + ms = modulestore('direct') + course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) + + verticals = ms.get_items(['i4x', 'edX', 'full', 'vertical', None, None]) + + # let's assert on the metadata_inheritance on an existing vertical + for vertical in verticals: + self.assertIn('xqa_key', vertical.metadata) + self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key']) + + self.assertGreater(len(verticals), 0) + + 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 and add it as a child to a vertical + ms.clone_item(source_template_location, new_component_location) + parent = verticals[0] + ms.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()]) + + # flush the cache + ms.get_cached_metadata_inheritance_tree(new_component_location, -1) + new_module = ms.get_item(new_component_location) + + # check for grace period definition which should be defined at the course level + self.assertIn('graceperiod', new_module.metadata) + + self.assertEqual(parent.metadata['graceperiod'], new_module.metadata['graceperiod']) + + self.assertEqual(course.metadata['xqa_key'], new_module.metadata['xqa_key']) + + # + # now let's define an override at the leaf node level + # + new_module.metadata['graceperiod'] = '1 day' + ms.update_metadata(new_module.location, new_module.metadata) + + # flush the cache and refetch + ms.get_cached_metadata_inheritance_tree(new_component_location, -1) + new_module = ms.get_item(new_component_location) + + self.assertIn('graceperiod', new_module.metadata) + self.assertEqual('1 day', new_module.metadata['graceperiod']) + + +class TemplateTestCase(ModuleStoreTestCase): + + def test_template_cleanup(self): + ms = modulestore('direct') + + # insert a bogus template in the store + bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus') + source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page') + + ms.clone_item(source_template_location, bogus_template_location) + + verify_create = ms.get_item(bogus_template_location) + self.assertIsNotNone(verify_create) + + # now run cleanup + update_templates() + + # now try to find dangling template, it should not be in DB any longer + asserted = False + try: + verify_create = ms.get_item(bogus_template_location) + except ItemNotFoundError: + asserted = True + + self.assertTrue(asserted) + + diff --git a/cms/djangoapps/contentstore/tests/test_core_caching.py b/cms/djangoapps/contentstore/tests/test_core_caching.py index 0cb4a4930c..676627a045 100644 --- a/cms/djangoapps/contentstore/tests/test_core_caching.py +++ b/cms/djangoapps/contentstore/tests/test_core_caching.py @@ -1,7 +1,8 @@ -from django.test.testcases import TestCase from cache_toolbox.core import get_cached_content, set_cached_content, del_cached_content from xmodule.modulestore import Location from xmodule.contentstore.content import StaticContent +from django.test import TestCase + class Content: def __init__(self, location, content): @@ -11,6 +12,7 @@ class Content: def get_id(self): return StaticContent.get_id_from_location(self.location) + class CachingTestCase(TestCase): # Tests for https://edx.lighthouseapp.com/projects/102637/tickets/112-updating-asset-does-not-refresh-the-cached-copy unicodeLocation = Location(u'c4x', u'mitX', u'800', u'thumbnail', u'monsters.jpg') @@ -32,7 +34,3 @@ class CachingTestCase(TestCase): 'should not be stored in cache with unicodeLocation') self.assertEqual(None, get_cached_content(self.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 74eff6e9cc..5560d2e39b 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -1,43 +1,57 @@ -from django.test.testcases import TestCase import datetime -import time +import json +import copy +from util import converters +from util.converters import jsdate_to_time + from django.contrib.auth.models import User -import xmodule from django.test.client import Client from django.core.urlresolvers import reverse -from xmodule.modulestore import Location -from cms.djangoapps.models.settings.course_details import CourseDetails,\ - CourseSettingsEncoder -import json -from util import converters -import calendar -from util.converters import jsdate_to_time from django.utils.timezone import UTC + +from xmodule.modulestore import Location +from cms.djangoapps.models.settings.course_details import (CourseDetails, + CourseSettingsEncoder) from cms.djangoapps.models.settings.course_grading import CourseGradingModel from cms.djangoapps.contentstore.utils import get_modulestore -import copy + +from django.test import TestCase +from utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + +from cms.djangoapps.models.settings.course_metadata import CourseMetadata +from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.django import modulestore + # 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()) - + 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): 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)) - - -class CourseTestCase(TestCase): + + +class CourseTestCase(ModuleStoreTestCase): 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. + """ uname = 'testuser' email = 'test+courses@edx.org' password = 'foo' @@ -52,36 +66,16 @@ class CourseTestCase(TestCase): self.user.is_staff = True self.user.save() - # 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 = {} - xmodule.modulestore.django.modulestore().collection.drop() - xmodule.templates.update_templates() - self.client = Client() self.client.login(username=uname, password=password) - self.course_data = { - 'template': 'i4x://edx/templates/course/Empty', - 'org': 'MITx', - 'number': '999', - 'display_name': 'Robot Super Course', - } - self.course_location = Location('i4x', 'MITx', '999', 'course', 'Robot_Super_Course') - self.create_course() + 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) - def tearDown(self): - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - - def create_course(self): - """Create new course""" - self.client.post(reverse('create_new_course'), self.course_data) class CourseDetailsTestCase(CourseTestCase): def test_virgin_fetch(self): @@ -94,7 +88,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview) self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video)) self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) - + def test_encoder(self): details = CourseDetails.fetch(self.course_location) jsondetails = json.dumps(details, cls=CourseSettingsEncoder) @@ -108,7 +102,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertEqual(jsondetails['overview'], "", "overview somehow initialized") self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized") self.assertIsNone(jsondetails['effort'], "effort somehow initialized") - + def test_update_and_fetch(self): ## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions jsondetails = CourseDetails.fetch(self.course_location) @@ -126,6 +120,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort, jsondetails.effort, "After set effort") + class CourseDetailsViewTest(CourseTestCase): def alter_field(self, url, details, field, val): setattr(details, field, val) @@ -136,9 +131,9 @@ class CourseDetailsViewTest(CourseTestCase): payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date) payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start) payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end) - resp = self.client.post(url, json.dumps(payload), "application/json") + resp = self.client.post(url, json.dumps(payload), "application/json") self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val)) - + @staticmethod def convert_datetime_to_iso(datetime): if datetime is not None: @@ -146,27 +141,22 @@ class CourseDetailsViewTest(CourseTestCase): else: return None - def test_update_and_fetch(self): details = CourseDetails.fetch(self.course_location) - - resp = self.client.get(reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, - 'name' : self.course_location.name })) - self.assertContains(resp, '
        1. Course Details
        2. ', status_code=200, html=True) - # resp s/b json from here on - url = reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, - 'name' : self.course_location.name, 'section' : 'details' }) + # resp s/b json from here on + url = reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course, + 'name': self.course_location.name, 'section': 'details'}) resp = self.client.get(url) self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get") utc = UTC() - self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,12,1,30, tzinfo=utc)) - self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,1,13,30, tzinfo=utc)) - self.alter_field(url, details, 'end_date', datetime.datetime(2013,2,12,1,30, tzinfo=utc)) - self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012,10,12,1,30, tzinfo=utc)) + self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=utc)) + self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=utc)) + self.alter_field(url, details, 'end_date', datetime.datetime(2013, 2, 12, 1, 30, tzinfo=utc)) + self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc)) - self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012,11,15,1,30, tzinfo=utc)) + self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=utc)) self.alter_field(url, details, 'overview', "Overview") self.alter_field(url, details, 'intro_video', "intro_video") self.alter_field(url, details, 'effort', "effort") @@ -179,7 +169,7 @@ class CourseDetailsViewTest(CourseTestCase): self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==") self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==") self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==") - + def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: if field in encoded and encoded[field] is not None: @@ -191,14 +181,15 @@ class CourseDetailsViewTest(CourseTestCase): else: details_encoded = jsdate_to_time(details[field]) dt2 = ConvertersTestCase.struct_to_datetime(details_encoded) - + expected_delta = datetime.timedelta(0) self.assertEqual(dt1 - dt2, expected_delta, str(dt1) + "!=" + str(dt2) + " at " + context) else: self.fail(field + " missing from encoded but in details at " + context) elif field in encoded and encoded[field] is not None: self.fail(field + " included in encoding but missing from details at " + context) - + + class CourseGradingTest(CourseTestCase): def test_initial_grader(self): descriptor = get_modulestore(self.course_location).get_item(self.course_location) @@ -218,58 +209,117 @@ class CourseGradingTest(CourseTestCase): self.assertEqual(self.course_location, test_grader.course_location, "Course locations") self.assertIsNotNone(test_grader.graders, "No graders") self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") - + for i, grader in enumerate(test_grader.graders): subgrader = CourseGradingModel.fetch_grader(self.course_location, i) self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal") - + subgrader = CourseGradingModel.fetch_grader(self.course_location.list(), 0) self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list") - + def test_fetch_cutoffs(self): test_grader = CourseGradingModel.fetch_cutoffs(self.course_location) # ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think) self.assertIsNotNone(test_grader, "No cutoffs via fetch") - + test_grader = CourseGradingModel.fetch_cutoffs(self.course_location.url()) self.assertIsNotNone(test_grader, "No cutoffs via fetch with url") - + def test_fetch_grace(self): test_grader = CourseGradingModel.fetch_grace_period(self.course_location) # almost a worthless test self.assertIn('grace_period', test_grader, "No grace via fetch") - + test_grader = CourseGradingModel.fetch_grace_period(self.course_location.url()) self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url") - + def test_update_from_json(self): test_grader = CourseGradingModel.fetch(self.course_location) altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update") - + test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2 altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2") - + test_grader.grade_cutoffs['D'] = 0.3 altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D") - - test_grader.grace_period = {'hours' : '4'} + + test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0} altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period") - + def test_update_grader_from_json(self): test_grader = CourseGradingModel.fetch(self.course_location) altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update") - + test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2 altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) self.assertDictEqual(test_grader.graders[1], altered_grader, "min_count[1] + 2") - + test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1 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) + # add in the full class too + 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') + self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") + + test_model = CourseMetadata.fetch(self.fullcourse_location) + self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in') + self.assertIn('display_name', test_model, 'full missing editable metadata field') + self.assertEqual(test_model['display_name'], 'Testing', "not expected value") + self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field') + self.assertIn('showanswer', test_model, 'showanswer field ') + self.assertIn('xqa_key', test_model, 'xqa_key field ') + + def test_update_from_json(self): + test_model = CourseMetadata.update_from_json(self.course_location, + { "a" : 1, + "b_a_c_h" : { "c" : "test" }, + "test_text" : "a text string"}) + 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, + { "a" : 2, + "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('a', test_model, 'Missing revised a metadata field') + self.assertEqual(test_model['a'], 2, "a not expected value") + + def update_check(self, test_model): + self.assertIn('display_name', test_model, 'Missing editable metadata field') + self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") + self.assertIn('a', test_model, 'Missing new a metadata field') + self.assertEqual(test_model['a'], 1, "a not expected value") + self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field') + self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value") + self.assertIn('test_text', test_model, 'Missing test_text metadata field') + self.assertEqual(test_model['test_text'], "a text string", "test_text not expected value") + + + def test_delete_key(self): + 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') + self.assertEqual(test_model['display_name'], 'Testing', "not expected value") + self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field') + # check for deletion effectiveness + self.assertNotIn('showanswer', test_model, 'showanswer field still in') + self.assertNotIn('xqa_key', test_model, 'xqa_key field still in') \ No newline at end of file diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py index 96e4468b31..c57f1322f5 100644 --- a/cms/djangoapps/contentstore/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/tests/test_course_updates.py @@ -2,29 +2,30 @@ from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCas from django.core.urlresolvers import reverse import json + class CourseUpdateTest(CourseTestCase): def test_course_update(self): # first get the update to force the creation - url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, - 'name' : self.course_location.name }) + url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, + 'name': self.course_location.name}) self.client.get(url) content = '' - payload = { 'content' : content, - 'date' : 'January 8, 2013'} - url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, - 'provided_id' : ''}) - + payload = {'content': content, + 'date': 'January 8, 2013'} + url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, + 'provided_id': ''}) + resp = self.client.post(url, json.dumps(payload), "application/json") - - payload= json.loads(resp.content) - + + payload = json.loads(resp.content) + self.assertHTMLEqual(content, payload['content'], "single iframe") - - url = reverse('course_info', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course, - 'provided_id' : payload['id']}) + + url = reverse('course_info', kwargs={'org': self.course_location.org, 'course': self.course_location.course, + 'provided_id': payload['id']}) content += '
          div

          p

          ' payload['content'] = content resp = self.client.post(url, json.dumps(payload), "application/json") - + self.assertHTMLEqual(content, json.loads(resp.content)['content'], "iframe w/ div") diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index 13f6189cc5..09e3b045f9 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -1,16 +1,17 @@ -from django.test.testcases import TestCase from cms.djangoapps.contentstore import utils import mock +from django.test import TestCase + class LMSLinksTestCase(TestCase): def about_page_test(self): - location = 'i4x','mitX','101','course', 'test' + location = 'i4x', 'mitX', '101', 'course', 'test' utils.get_course_id = mock.Mock(return_value="mitX/101/test") link = utils.get_lms_link_for_about_page(location) self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/about") def ls_link_test(self): - location = 'i4x','mitX','101','vertical', 'contacting_us' + location = 'i4x', 'mitX', '101', 'vertical', 'contacting_us' utils.get_course_id = mock.Mock(return_value="mitX/101/test") link = utils.get_lms_link_for_item(location, False) self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us") diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 2597ac64fd..c4a46459e2 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -1,46 +1,33 @@ import json import shutil -from django.test import TestCase from django.test.client import Client -from override_settings import override_settings from django.conf import settings from django.core.urlresolvers import reverse from path import path -from tempfile import mkdtemp import json - -from student.models import Registration -from django.contrib.auth.models import User -import xmodule.modulestore.django -from xmodule.modulestore.xml_importer import import_from_xml +from fs.osfs import OSFS import copy -from factories import * +from cms.djangoapps.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 +from xmodule.modulestore.django import modulestore, _MODULESTORES from xmodule.contentstore.django import contentstore -from xmodule.course_module import CourseDescriptor +from xmodule.templates import update_templates from xmodule.modulestore.xml_exporter import export_to_xml -from cms.djangoapps.contentstore.utils import get_modulestore +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 -def parse_json(response): - """Parse response, which is assumed to be json""" - return json.loads(response.content) +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from utils import ModuleStoreTestCase, parse_json, user, registration -def user(email): - """look up a user by email""" - return User.objects.get(email=email) - - -def registration(email): - """look up registration object by email""" - return Registration.objects.get(user__email=email) - - -class ContentStoreTestCase(TestCase): +class ContentStoreTestCase(ModuleStoreTestCase): def _login(self, email, pw): """Login. View should always return 200. The success/fail is in the returned json""" @@ -97,7 +84,6 @@ class ContentStoreTestCase(TestCase): # 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""" @@ -185,294 +171,3 @@ class AuthTestCase(ContentStoreTestCase): self.assertEqual(resp.status_code, 302) # Logged in should work. - -TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) -TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') -TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data') - -@override_settings(MODULESTORE=TEST_DATA_MODULESTORE) -class ContentStoreTest(TestCase): - - def setUp(self): - uname = 'testuser' - email = 'test+courses@edx.org' - password = 'foo' - - # Create the use so we can log them in. - self.user = User.objects.create_user(uname, email, 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() - - # 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 = {} - xmodule.modulestore.django.modulestore().collection.drop() - xmodule.templates.update_templates() - - self.client = Client() - self.client.login(username=uname, password=password) - - self.course_data = { - 'template': 'i4x://edx/templates/course/Empty', - 'org': 'MITx', - 'number': '999', - 'display_name': 'Robot Super Course', - } - - def tearDown(self): - # Make sure you flush out the test modulestore after the end - # of the last test because otherwise on the next run - # cms/djangoapps/contentstore/__init__.py - # update_templates() will try to update the templates - # via upsert and it sometimes seems to be messing things up. - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - - def test_create_course(self): - """Test new course creation - happy path""" - resp = self.client.post(reverse('create_new_course'), self.course_data) - self.assertEqual(resp.status_code, 200) - data = parse_json(resp) - self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') - - def test_create_course_duplicate_course(self): - """Test new course creation - error path""" - resp = self.client.post(reverse('create_new_course'), self.course_data) - resp = self.client.post(reverse('create_new_course'), self.course_data) - data = parse_json(resp) - self.assertEqual(resp.status_code, 200) - self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.') - - def test_create_course_duplicate_number(self): - """Test new course creation - error path""" - resp = self.client.post(reverse('create_new_course'), self.course_data) - self.course_data['display_name'] = 'Robot Super Course Two' - - resp = self.client.post(reverse('create_new_course'), self.course_data) - data = parse_json(resp) - - self.assertEqual(resp.status_code, 200) - self.assertEqual(data['ErrMsg'], - '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""" - self.course_data['org'] = 'University of California, Berkeley' - resp = self.client.post(reverse('create_new_course'), self.course_data) - data = parse_json(resp) - - 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'.") - - 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, - '

          My Courses

          ', - status_code=200, - html=True) - - def test_course_factory(self): - course = CourseFactory.create() - self.assertIsInstance(course, xmodule.course_module.CourseDescriptor) - - def test_item_factory(self): - course = CourseFactory.create() - item = ItemFactory.create(parent_location=course.location) - self.assertIsInstance(item, xmodule.seq_module.SequenceDescriptor) - - def test_course_index_view_with_course(self): - """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, - 'Robot Super Educational Course', - status_code=200, - 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'), - } - - resp = self.client.get(reverse('course_index', kwargs=data)) - self.assertContains(resp, - 'Robot Super Course', - status_code=200, - html=True) - - def test_clone_item(self): - """Test cloning an item. E.g. creating a new section""" - CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') - - section_data = { - '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}$') - - def check_edit_unit(self, test_course_name): - import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) - - for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)): - print "Checking ", descriptor.location.url() - print descriptor.__class__, descriptor.location - resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()})) - self.assertEqual(resp.status_code, 200) - - def test_edit_unit_toy(self): - self.check_edit_unit('toy') - - def test_edit_unit_full(self): - self.check_edit_unit('full') - - 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 - while there is a base definition in /about/effort.html - ''' - import_from_xml(modulestore(), 'common/test/data/', ['full']) - ms = modulestore('direct') - effort = ms.get_item(Location(['i4x','edX','full','about','effort', None])) - self.assertEqual(effort.definition['data'],'6 hours') - - # this one should be in a non-override folder - effort = ms.get_item(Location(['i4x','edX','full','about','end_date', None])) - self.assertEqual(effort.definition['data'],'TBD') - - - def test_clone_course(self): - import_from_xml(modulestore(), 'common/test/data/', ['full']) - - resp = self.client.post(reverse('create_new_course'), self.course_data) - self.assertEqual(resp.status_code, 200) - data = parse_json(resp) - self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') - - ms = modulestore('direct') - cs = contentstore() - - source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') - dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course') - - clone_course(ms, cs, source_location, dest_location) - - # now loop through all the units in the course and verify that the clone can render them, which - # means the objects are at least present - items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None])) - self.assertGreater(len(items), 0) - clone_items = ms.get_items(Location(['i4x', 'MITx','999','vertical', None])) - self.assertGreater(len(clone_items), 0) - for descriptor in items: - new_loc = descriptor.location._replace(org = 'MITx', course='999') - print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url()) - resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()})) - self.assertEqual(resp.status_code, 200) - - def test_delete_course(self): - import_from_xml(modulestore(), 'common/test/data/', ['full']) - - ms = modulestore('direct') - cs = contentstore() - - location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') - - delete_course(ms, cs, location) - - items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None])) - self.assertEqual(len(items), 0) - - def test_export_course(self): - ms = modulestore('direct') - cs = contentstore() - - import_from_xml(ms, 'common/test/data/', ['full']) - location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') - - root_dir = path(mkdtemp()) - - print 'Exporting to tempdir = {0}'.format(root_dir) - - # export out to a tempdir - export_to_xml(ms, cs, location, root_dir, 'test_export') - - # remove old course - delete_course(ms, cs, location) - - # reimport - import_from_xml(ms, root_dir, ['test_export']) - - items = ms.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) - - shutil.rmtree(root_dir) - - def test_course_handouts_rewrites(self): - ms = modulestore('direct') - cs = contentstore() - - # import a test course - import_from_xml(ms, 'common/test/data/', ['full']) - - handout_location= Location(['i4x', 'edX', 'full', 'course_info', 'handouts']) - - # get module info - resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location})) - - # make sure we got a successful response - self.assertEqual(resp.status_code, 200) - - # check that /static/ has been converted to the full path - # 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_capa_module(self): - """Test that a problem treats markdown specially.""" - CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') - - problem_data = { - 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course', - 'template' : 'i4x://edx/templates/problem/Empty' - } - - resp = self.client.post(reverse('clone_item'), problem_data) - - self.assertEqual(resp.status_code, 200) - payload = parse_json(resp) - problem_loc = payload['id'] - problem = get_modulestore(problem_loc).get_item(problem_loc) - # should be a CapaDescriptor - self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor") - context = problem.get_context() - self.assertIn('markdown', context, "markdown is missing from context") - self.assertIn('markdown', problem.metadata, "markdown is missing from metadata") - self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields") \ No newline at end of file diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py new file mode 100644 index 0000000000..b6b8cd5023 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -0,0 +1,65 @@ +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""" + return json.loads(response.content) + + +def user(email): + """look up a user by email""" + return User.objects.get(email=email) + + +def registration(email): + """look up registration object by email""" + return Registration.objects.get(user__email=email) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index da2993e463..cba30131b5 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -5,18 +5,20 @@ from xmodule.modulestore.exceptions import ItemNotFoundError DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] + def get_modulestore(location): """ Returns the correct modulestore to use for modifying the specified location """ if not isinstance(location, Location): location = Location(location) - + if location.category in DIRECT_ONLY_CATEGORIES: return modulestore('direct') else: return modulestore() + def get_course_location_for_item(location): ''' cdodge: for a given Xmodule, return the course that it belongs to @@ -46,6 +48,7 @@ def get_course_location_for_item(location): return location + def get_course_for_item(location): ''' cdodge: for a given Xmodule, return the course that it belongs to @@ -72,12 +75,20 @@ def get_course_for_item(location): return courses[0] -def get_lms_link_for_item(location, preview=False): +def get_lms_link_for_item(location, preview=False, course_id=None): + if course_id is None: + course_id = get_course_id(location) + if settings.LMS_BASE is not None: - lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format( - preview='preview.' if preview else '', - lms_base=settings.LMS_BASE, - course_id=get_course_id(location), + if preview: + 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, location=Location(location) ) else: @@ -85,6 +96,7 @@ def get_lms_link_for_item(location, preview=False): return lms_link + def get_lms_link_for_about_page(location): """ Returns the url to the course about page from the location tuple. @@ -99,6 +111,7 @@ def get_lms_link_for_about_page(location): return lms_link + def get_course_id(location): """ Returns the course_id from a given the location tuple. @@ -106,6 +119,7 @@ def get_course_id(location): # TODO: These will need to be changed to point to the particular instance of this problem in the particular course return modulestore().get_containing_courses(Location(location))[0].id + class UnitState(object): draft = 'draft' private = 'private' @@ -135,6 +149,7 @@ def compute_unit_state(unit): 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. @@ -142,4 +157,4 @@ def update_item(location, value): if value is None: get_modulestore(location).delete_item(location) else: - get_modulestore(location).update_item(location, value) \ No newline at end of file + get_modulestore(location).update_item(location, value) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 816ccab091..34003d71a4 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -31,7 +31,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr from xmodule.x_module import ModuleSystem from xmodule.error_module import ErrorDescriptor from xmodule.errortracker import exc_info_to_str -from static_replace import replace_urls +import static_replace from external_auth.views import ssl_login_shortcut from mitxmako.shortcuts import render_to_response, render_to_string @@ -58,7 +58,8 @@ from cms.djangoapps.models.settings.course_details import CourseDetails,\ CourseSettingsEncoder from cms.djangoapps.models.settings.course_grading import CourseGradingModel from cms.djangoapps.contentstore.utils import get_modulestore -from lxml import etree +from django.shortcuts import redirect +from cms.djangoapps.models.settings.course_metadata import CourseMetadata # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' @@ -67,6 +68,10 @@ log = logging.getLogger(__name__) COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] +ADVANCED_COMPONENT_TYPES = ['annotatable','combinedopenended', 'peergrading'] +ADVANCED_COMPONENT_CATEGORY = 'advanced' +ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' + # cdodge: these are categories which should not be parented, they are detached from the hierarchy DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] @@ -81,6 +86,12 @@ def signup(request): csrf_token = csrf(request)['csrf_token'] return render_to_response('signup.html', {'csrf': csrf_token}) +def old_login_redirect(request): + ''' + Redirect to the active login url. + ''' + return redirect('login', permanent=True) + @ssl_login_shortcut @ensure_csrf_cookie def login_page(request): @@ -93,6 +104,11 @@ def login_page(request): 'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE), }) +def howitworks(request): + if request.user.is_authenticated(): + return index(request) + else: + return render_to_response('howitworks.html', {}) # ==== Views for any logged-in user ================================== @@ -102,7 +118,7 @@ def index(request): """ List all courses available to the logged in user """ - courses = modulestore().get_items(['i4x', None, None, 'course', None]) + courses = modulestore('direct').get_items(['i4x', None, None, 'course', None]) # filter out courses that we don't have access too def course_filter(course): @@ -114,14 +130,16 @@ def index(request): courses = filter(course_filter, courses) return render_to_response('index.html', { - 'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'), + 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'), 'courses': [(course.metadata.get('display_name'), reverse('course_index', args=[ course.location.org, course.location.course, - course.location.name])) + course.location.name]), + get_lms_link_for_item(course.location, course_id=course.location.course_id)) for course in courses], - 'user': request.user + 'user': request.user, + 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff }) @@ -132,7 +150,7 @@ def has_access(user, location, role=STAFF_ROLE_NAME): Return True if user allowed to access this piece of data Note that the CMS permissions model is with respect to courses There is a super-admin permissions if user.is_staff is set - Also, since we're unifying the user database between LMS and CAS, + Also, since we're unifying the user database between LMS and CAS, I'm presuming that the course instructor (formally known as admin) will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR has all the rights that STAFF do @@ -154,15 +172,17 @@ def course_index(request, org, course, name): org, course, name: Attributes of the Location for the item to edit """ location = ['i4x', org, course, 'course', name] - + # check that logged in user has permissions to this item if not has_access(request.user, location): raise PermissionDenied() - upload_asset_callback_url = reverse('upload_asset', kwargs = { - 'org' : org, - 'course' : course, - 'coursename' : name + lms_link = get_lms_link_for_item(location) + + upload_asset_callback_url = reverse('upload_asset', kwargs={ + 'org': org, + 'course': course, + 'coursename': name }) course = modulestore().get_item(location) @@ -171,6 +191,7 @@ def course_index(request, org, course, name): return render_to_response('overview.html', { 'active_tab': 'courseware', 'context_course': course, + 'lms_link': lms_link, 'sections': sections, 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), 'parent_location': course.location, @@ -213,7 +234,7 @@ def edit_subsection(request, location): # remove all metadata from the generic dictionary that is presented in a more normalized UI - policy_metadata = dict((key,value) for key, value in item.metadata.iteritems() + policy_metadata = dict((key, value) for key, value in item.metadata.iteritems() if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields) can_view_live = False @@ -233,9 +254,9 @@ def edit_subsection(request, location): '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 + 'policy_metadata': policy_metadata, + 'subsection_units': subsection_units, + 'can_view_live': can_view_live }) @@ -261,18 +282,38 @@ def edit_unit(request, location): break lms_link = get_lms_link_for_item(item.location) - preview_lms_link = get_lms_link_for_item(item.location, preview=True) component_templates = defaultdict(list) + # Check if there are any advanced modules specified in the course policy. These modules + # should be specified as a list of strings, where the strings are the names of the modules + # in ADVANCED_COMPONENT_TYPES that should be enabled for the course. + course_metadata = CourseMetadata.fetch(course.location) + course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, []) + + # Set component types according to course policy file + component_types = list(COMPONENT_TYPES) + if isinstance(course_advanced_keys, list): + course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES] + if len(course_advanced_keys) > 0: + component_types.append(ADVANCED_COMPONENT_CATEGORY) + else: + log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys)) + templates = modulestore().get_items(Location('i4x', 'edx', 'templates')) for template in templates: - if template.location.category in COMPONENT_TYPES: - component_templates[template.location.category].append(( + category = template.location.category + + if category in course_advanced_keys: + category = ADVANCED_COMPONENT_CATEGORY + + if category in component_types: + #This is a hack to create categories for different xmodules + component_templates[category].append(( template.display_name, template.location.url(), 'markdown' in template.metadata, - template.location.name == 'Empty' + 'empty' in template.metadata )) components = [ @@ -292,23 +333,26 @@ def edit_unit(request, location): containing_section = modulestore().get_item(containing_section_locs[0]) # cdodge hack. We're having trouble previewing drafts via jump_to redirect - # so let's generate the link url here + # so let's generate the link url here # need to figure out where this item is in the list of children as the preview will need this - index =1 + index = 1 for child in containing_subsection.get_children(): if child.location == item.location: break index = index + 1 - preview_lms_link = '//{preview}{lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format( - preview='preview.', - lms_base=settings.LMS_BASE, + preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_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, + 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) @@ -359,14 +403,14 @@ def assignment_type_update(request, org, course, category, name): location = Location(['i4x', org, course, category, name]) if not has_access(request.user, location): raise HttpResponseForbidden() - + if request.method == 'GET': - return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)), + return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)), mimetype="application/json") - elif request.method == 'POST': # post or put, doesn't matter. - return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)), + elif request.method == 'POST': # post or put, doesn't matter. + return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)), mimetype="application/json") - + def user_author_string(user): '''Get an author string for commits by this user. Format: @@ -474,7 +518,7 @@ def preview_module_system(request, preview_id, descriptor): get_module=partial(get_preview_module, request, preview_id), render_template=render_from_lms, debug=True, - replace_urls=replace_urls, + replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location), user=request.user, ) @@ -511,24 +555,24 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_ error_msg=exc_info_to_str(sys.exc_info()) ).xmodule_constructor(system)(None, None) - # cdodge: Special case + # cdodge: Special case if module.location.category == 'static_tab': module.get_html = wrap_xmodule( module.get_html, module, "xmodule_tab_display.html", ) - else: + else: module.get_html = wrap_xmodule( module.get_html, module, "xmodule_display.html", ) - + module.get_html = replace_static_urls( module.get_html, module.metadata.get('data_dir', module.location.course), - course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None]) + course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None]) ) save_preview_state(request, preview_id, descriptor.location.url(), module.get_instance_state(), module.get_shared_state()) @@ -555,7 +599,7 @@ def _xmodule_recurse(item, action): _xmodule_recurse(child, action) action(item) - + @login_required @expect_json @@ -589,8 +633,8 @@ def delete_item(request): # 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) + if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions: + modulestore('direct').delete_item(item.location) return HttpResponse() @@ -609,7 +653,7 @@ def save_item(request): if request.POST.get('data') is not None: data = request.POST['data'] store.update_item(item_location, data) - + # cdodge: note calling request.POST.get('children') will return None if children is an empty array # so it lead to a bug whereby the last component to be deleted in the UI was not actually # deleting the children object from the children collection @@ -699,7 +743,7 @@ def unpublish_unit(request): def clone_item(request): parent_location = Location(request.POST['parent_location']) template = Location(request.POST['template']) - + display_name = request.POST.get('display_name') if not has_access(request.user, parent_location): @@ -739,9 +783,9 @@ def upload_asset(request, org, course, coursename): location = ['i4x', org, course, 'course', coursename] if not has_access(request.user, location): return HttpResponseForbidden() - + # Does the course actually exist?!? Get anything from it to prove its existance - + try: item = modulestore().get_item(location) except: @@ -775,12 +819,12 @@ def upload_asset(request, org, course, coursename): # readback the saved content - we need the database timestamp 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' + + 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' } response = HttpResponse(json.dumps(response_payload)) @@ -793,7 +837,7 @@ This view will return all CMS users who are editors for the specified course @login_required @ensure_csrf_cookie def manage_users(request, location): - + # 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() @@ -804,16 +848,16 @@ def manage_users(request, location): 'active_tab': 'users', 'context_course': course_module, 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME), - 'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'), - 'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/'), - 'allow_actions' : has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME), - 'request_user_id' : request.user.id + 'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'), + 'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'), + 'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME), + 'request_user_id': request.user.id }) - + def create_json_response(errmsg = None): if errmsg is not None: - resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg' : errmsg})) + resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg})) else: resp = HttpResponse(json.dumps({'Status': 'OK'})) @@ -829,15 +873,15 @@ the specified course def add_user(request, location): email = request.POST["email"] - if email=='': + if email == '': return create_json_response('Please specify an email address.') - + # check that logged in user has admin permissions to this course if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): raise PermissionDenied() - + user = get_user_by_email(email) - + # user doesn't exist?!? Return error. if user is None: return create_json_response('Could not find user by email address \'{0}\'.'.format(email)) @@ -860,7 +904,7 @@ the specified course @ensure_csrf_cookie def remove_user(request, location): email = request.POST["email"] - + # check that logged in user has admin permissions on this course if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): raise PermissionDenied() @@ -887,7 +931,7 @@ def landing(request, org, course, coursename): def static_pages(request, org, course, coursename): location = ['i4x', org, course, 'course', coursename] - + # check that logged in user has permissions to this item if not has_access(request.user, location): raise PermissionDenied() @@ -903,10 +947,56 @@ def static_pages(request, org, course, coursename): def edit_static(request, org, course, coursename): return render_to_response('edit-static-page.html', {}) + +@login_required +@expect_json +def reorder_static_tabs(request): + tabs = request.POST['tabs'] + course = get_course_for_item(tabs[0]) + + if not has_access(request.user, course.location): + raise PermissionDenied() + + # get list of existing static tabs in course + # make sure they are the same lengths (i.e. the number of passed in tabs equals the number + # that we know about) otherwise we can drop some! + + existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab'] + if len(existing_static_tabs) != len(tabs): + return HttpResponseBadRequest() + + # load all reference tabs, return BadRequest if we can't find any of them + tab_items = [] + for tab in tabs: + item = modulestore('direct').get_item(Location(tab)) + if item is None: + return HttpResponseBadRequest() + + tab_items.append(item) + + # now just go through the existing course_tabs and re-order the static tabs + reordered_tabs = [] + static_tab_idx = 0 + for tab in course.tabs: + if tab['type'] == 'static_tab': + reordered_tabs.append({'type': 'static_tab', + 'name': tab_items[static_tab_idx].metadata.get('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, course.metadata) + return HttpResponse() + + @login_required @ensure_csrf_cookie def edit_tabs(request, org, course, coursename): - location = ['i4x', org, course, 'course', coursename] + location = ['i4x', org, course, 'course', coursename] course_item = modulestore().get_item(location) static_tabs_loc = Location('i4x', org, course, 'static_tab', None) @@ -914,12 +1004,19 @@ def edit_tabs(request, org, course, coursename): if not has_access(request.user, location): raise PermissionDenied() - static_tabs = modulestore('direct').get_items(static_tabs_loc) - # see tabs have been uninitialized (e.g. supporing courses created before tab support in studio) if course_item.tabs is None or len(course_item.tabs) == 0: initialize_course_tabs(course_item) + # first get all static tabs from the tabs list + # we do this because this is also the order in which items are displayed in the LMS + static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab'] + + static_tabs = [] + for static_tab_ref in static_tabs_refs: + static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug']) + static_tabs.append(modulestore('direct').get_item(static_tab_loc)) + components = [ static_tab.location.url() for static_tab @@ -928,7 +1025,7 @@ def edit_tabs(request, org, course, coursename): return render_to_response('edit-tabs.html', { 'active_tab': 'pages', - 'context_course':course_item, + 'context_course': course_item, 'components': components }) @@ -949,24 +1046,24 @@ def course_info(request, org, course, name, provided_id=None): org, course, name: Attributes of the Location for the item to edit """ location = ['i4x', org, course, 'course', name] - + # check that logged in user has permissions to this item if not has_access(request.user, location): raise PermissionDenied() - + course_module = modulestore().get_item(location) - + # get current updates location = ['i4x', org, course, 'course_info', "updates"] return render_to_response('course_info.html', { 'active_tab': 'courseinfo-tab', 'context_course': course_module, - 'url_base' : "/" + org + "/" + course + "/", - 'course_updates' : json.dumps(get_course_updates(location)), + 'url_base': "/" + org + "/" + course + "/", + 'course_updates': json.dumps(get_course_updates(location)), 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url() }) - + @expect_json @login_required @ensure_csrf_cookie @@ -980,7 +1077,7 @@ def course_info_updates(request, org, course, provided_id=None): # ??? No way to check for access permission afaik # get current updates location = ['i4x', org, course, 'course_info', "updates"] - + # Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-( # Possibly due to my removing the seemingly redundant pattern in urls.py if provided_id == '': @@ -995,7 +1092,7 @@ def course_info_updates(request, org, course, provided_id=None): real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE'] else: real_method = request.method - + if request.method == 'GET': return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json") elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE @@ -1012,7 +1109,7 @@ def course_info_updates(request, org, course, provided_id=None): @ensure_csrf_cookie def module_info(request, module_location): location = Location(module_location) - + # check that logged in user has permissions to this item if not has_access(request.user, location): raise PermissionDenied() @@ -1023,12 +1120,12 @@ def module_info(request, module_location): else: real_method = request.method - rewrite_static_links = request.GET.get('rewrite_url_links','True') in ['True', 'true'] - logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links','False'), rewrite_static_links)) - + rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true'] + logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links)) + # check that logged in user has permissions to this item if not has_access(request.user, location): - raise PermissionDenied() + raise PermissionDenied() if real_method == 'GET': return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json") @@ -1037,6 +1134,7 @@ def module_info(request, module_location): else: return HttpResponseBadRequest() + @login_required @ensure_csrf_cookie def get_course_settings(request, org, course, name): @@ -1046,20 +1144,69 @@ def get_course_settings(request, org, course, name): org, course, name: Attributes of the Location for the item to edit """ location = ['i4x', org, course, 'course', name] - + # check that logged in user has permissions to this item if not has_access(request.user, location): raise PermissionDenied() - + course_module = modulestore().get_item(location) - course_details = CourseDetails.fetch(location) - + return render_to_response('settings.html', { - 'active_tab': 'settings', 'context_course': course_module, - 'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder) + 'course_location': location, + 'details_url': reverse(course_settings_updates, + kwargs={"org": org, + "course": course, + "name": name, + "section": "details"}) }) - + +@login_required +@ensure_csrf_cookie +def course_config_graders_page(request, org, course, name): + """ + Send models and views as well as html for editing the course settings to the client. + + org, course, name: Attributes of the Location for the item to edit + """ + location = ['i4x', org, course, 'course', name] + + # check that logged in user has permissions to this item + if not has_access(request.user, location): + raise PermissionDenied() + + course_module = modulestore().get_item(location) + course_details = CourseGradingModel.fetch(location) + + return render_to_response('settings_graders.html', { + 'context_course': course_module, + 'course_location' : location, + 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder) + }) + +@login_required +@ensure_csrf_cookie +def course_config_advanced_page(request, org, course, name): + """ + Send models and views as well as html for editing the advanced course settings to the client. + + org, course, name: Attributes of the Location for the item to edit + """ + location = ['i4x', org, course, 'course', name] + + # check that logged in user has permissions to this item + if not has_access(request.user, location): + raise PermissionDenied() + + course_module = modulestore().get_item(location) + + return render_to_response('settings_advanced.html', { + 'context_course': course_module, + 'course_location' : location, + 'advanced_blacklist' : json.dumps(CourseMetadata.FILTERED_LIST), + 'advanced_dict' : json.dumps(CourseMetadata.fetch(location)), + }) + @expect_json @login_required @ensure_csrf_cookie @@ -1082,13 +1229,13 @@ def course_settings_updates(request, org, course, name, section): elif section == 'grading': manager = CourseGradingModel else: return - + if request.method == 'GET': # Cannot just do a get w/o knowing the course name :-( - return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseSettingsEncoder), + return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder), mimetype="application/json") - elif request.method == 'POST': # post or put, doesn't matter. - return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder), + elif request.method == 'POST': # post or put, doesn't matter. + return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder), mimetype="application/json") @expect_json @@ -1101,7 +1248,7 @@ def course_grader_updates(request, org, course, name, grader_index=None): org, course: Attributes of the Location for the item to edit """ - + location = ['i4x', org, course, 'course', name] # check that logged in user has permissions to this item @@ -1112,19 +1259,50 @@ def course_grader_updates(request, org, course, name, grader_index=None): real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE'] else: real_method = request.method - + if real_method == 'GET': # Cannot just do a get w/o knowing the course name :-( - return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course',name]), grader_index)), + return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course', name]), grader_index)), mimetype="application/json") elif real_method == "DELETE": - # ??? Shoudl this return anything? Perhaps success fail? - CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course',name]), grader_index) + # ??? Shoudl this return anything? Perhaps success fail? + CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course', name]), grader_index) return HttpResponse() - elif request.method == 'POST': # post or put, doesn't matter. - return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course',name]), request.POST)), + elif request.method == 'POST': # post or put, doesn't matter. + return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)), mimetype="application/json") + +## NB: expect_json failed on ["key", "key2"] and json payload +@login_required +@ensure_csrf_cookie +def course_advanced_updates(request, org, course, name): + """ + restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh, + the payload is either a key or a list of keys to delete. + + org, course: Attributes of the Location for the item to edit + """ + location = ['i4x', org, course, 'course', name] + + # check that logged in user has permissions to this item + if not has_access(request.user, location): + raise PermissionDenied() + + # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!! + if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META: + real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE'] + else: + real_method = request.method + + 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") + 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") + @login_required @ensure_csrf_cookie @@ -1135,20 +1313,20 @@ def asset_index(request, org, course, name): org, course, name: Attributes of the Location for the item to edit """ location = ['i4x', org, course, 'course', name] - + # check that logged in user has permissions to this item if not has_access(request.user, location): raise PermissionDenied() - upload_asset_callback_url = reverse('upload_asset', kwargs = { - 'org' : org, - 'course' : course, - 'coursename' : name + upload_asset_callback_url = reverse('upload_asset', kwargs={ + 'org': org, + 'course': course, + 'coursename': name }) course_module = modulestore().get_item(location) - + course_reference = StaticContent.compute_location(org, course, name) assets = contentstore().get_all_content_for_course(course_reference) @@ -1162,15 +1340,15 @@ def asset_index(request, org, course, name): display_info = {} display_info['displayname'] = asset['displayname'] display_info['uploadDate'] = get_date_display(asset['uploadDate']) - + asset_location = StaticContent.compute_location(id['org'], id['course'], id['name']) display_info['url'] = StaticContent.get_url_path_from_location(asset_location) - + # note, due to the schema change we may not have a 'thumbnail_location' in the result set _thumbnail_location = asset.get('thumbnail_location', None) thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None - + asset_display.append(display_info) return render_to_response('asset_index.html', { @@ -1188,10 +1366,19 @@ def edge(request): @login_required @expect_json def create_new_course(request): + + if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff: + raise PermissionDenied() + + # This logic is repeated in xmodule/modulestore/tests/factories.py + # so if you change anything here, you need to also change it there. + # TODO: write a test that creates two courses, one with the factory and + # the other with this method, then compare them to make sure they are + # equivalent. template = Location(request.POST['template']) - org = request.POST.get('org') - number = request.POST.get('number') - display_name = request.POST.get('display_name') + org = request.POST.get('org') + number = request.POST.get('number') + display_name = request.POST.get('display_name') try: dest_location = Location('i4x', org, number, 'course', Location.clean(display_name)) @@ -1237,13 +1424,16 @@ def initialize_course_tabs(course): # at least a list populated with the minimal times # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here - course.tabs = [{"type": "courseware"}, + + # 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"}] - modulestore('direct').update_metadata(course.location.url(), course.own_metadata) + modulestore('direct').update_metadata(course.location.url(), course.own_metadata) @ensure_csrf_cookie @login_required @@ -1283,7 +1473,7 @@ def import_course(request, org, course, name): # find the 'course.xml' file - for r,d,f in os.walk(course_dir): + for r, d, f in os.walk(course_dir): for files in f: if files == 'course.xml': break @@ -1297,10 +1487,10 @@ def import_course(request, org, course, name): if r != course_dir: for fname in os.listdir(r): - shutil.move(r/fname, course_dir) + 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)) # we can blow this away when we're done importing. shutil.rmtree(course_dir) @@ -1316,7 +1506,7 @@ def import_course(request, org, course, name): return render_to_response('import.html', { 'context_course': course_module, 'active_tab': 'import', - 'successful_import_redirect_url' : reverse('course_index', args=[ + 'successful_import_redirect_url': reverse('course_index', args=[ course_module.location.org, course_module.location.course, course_module.location.name]) @@ -1326,18 +1516,17 @@ def import_course(request, org, course, name): @login_required def generate_export_course(request, org, course, name): location = ['i4x', org, course, 'course', name] - course_module = modulestore().get_item(location) # check that logged in user has permissions to this item if not has_access(request.user, location): raise PermissionDenied() loc = Location(location) - export_file = NamedTemporaryFile(prefix=name+'.', suffix=".tar.gz") + export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz") root_dir = path(mkdtemp()) # export out to a tempdir - + logging.debug('root = {0}'.format(root_dir)) export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name) @@ -1345,11 +1534,11 @@ def generate_export_course(request, org, course, name): logging.debug('tar file being generated at {0}'.format(export_file.name)) tf = tarfile.open(name=export_file.name, mode='w:gz') - tf.add(root_dir/name, arcname=name) + tf.add(root_dir / name, arcname=name) tf.close() # remove temp dir - shutil.rmtree(root_dir/name) + shutil.rmtree(root_dir / name) wrapper = FileWrapper(export_file) response = HttpResponse(wrapper, content_type='application/x-tgz') @@ -1371,5 +1560,12 @@ def export_course(request, org, course, name): return render_to_response('export.html', { 'context_course': course_module, 'active_tab': 'export', - 'successful_import_redirect_url' : '' + 'successful_import_redirect_url': '' }) + +def event(request): + ''' + A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at + console logs don't get distracted :-) + ''' + return HttpResponse(True) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index d01e784d74..b27f4e3804 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -31,16 +31,16 @@ class CourseDetails(object): """ if not isinstance(course_location, Location): course_location = Location(course_location) - + course = cls(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) - + course.start_date = descriptor.start course.end_date = descriptor.end course.enrollment_start = descriptor.enrollment_start course.enrollment_end = descriptor.enrollment_end - + temploc = course_location._replace(category='about', name='syllabus') try: course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data'] @@ -52,32 +52,32 @@ class CourseDetails(object): course.overview = get_modulestore(temploc).get_item(temploc).definition['data'] except ItemNotFoundError: pass - + temploc = temploc._replace(name='effort') try: course.effort = get_modulestore(temploc).get_item(temploc).definition['data'] except ItemNotFoundError: pass - + temploc = temploc._replace(name='video') try: raw_video = get_modulestore(temploc).get_item(temploc).definition['data'] - course.intro_video = CourseDetails.parse_video_tag(raw_video) + course.intro_video = CourseDetails.parse_video_tag(raw_video) except ItemNotFoundError: pass - + return course - + @classmethod def update_from_json(cls, jsondict): """ Decode the json into CourseDetails and save any changed attrs to the db """ - ## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore + ## TODO make it an error for this to be undefined & for it to not be retrievable from modulestore course_location = jsondict['course_location'] ## Will probably want to cache the inflight courses because every blur generates an update descriptor = get_modulestore(course_location).get_item(course_location) - + dirty = False if 'start_date' in jsondict: @@ -87,7 +87,7 @@ class CourseDetails(object): if converted != descriptor.start: dirty = True descriptor.start = converted - + if 'end_date' in jsondict: converted = jsdate_to_time(jsondict['end_date']) else: @@ -96,7 +96,7 @@ class CourseDetails(object): if converted != descriptor.end: dirty = True descriptor.end = converted - + if 'enrollment_start' in jsondict: converted = jsdate_to_time(jsondict['enrollment_start']) else: @@ -105,7 +105,7 @@ class CourseDetails(object): if converted != descriptor.enrollment_start: dirty = True descriptor.enrollment_start = converted - + if 'enrollment_end' in jsondict: converted = jsdate_to_time(jsondict['enrollment_end']) else: @@ -114,10 +114,10 @@ class CourseDetails(object): if converted != descriptor.enrollment_end: dirty = True descriptor.enrollment_end = converted - + if dirty: get_modulestore(course_location).update_metadata(course_location, descriptor.metadata) - + # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. temploc = Location(course_location)._replace(category='about', name='syllabus') @@ -125,19 +125,19 @@ class CourseDetails(object): temploc = temploc._replace(name='overview') update_item(temploc, jsondict['overview']) - + temploc = temploc._replace(name='effort') update_item(temploc, jsondict['effort']) - + temploc = temploc._replace(name='video') recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) update_item(temploc, recomposed_video_tag) - - + + # 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 return CourseDetails.fetch(course_location) - + @staticmethod def parse_video_tag(raw_video): """ @@ -147,17 +147,17 @@ class CourseDetails(object): """ if not raw_video: return None - + keystring_matcher = re.search('(?<=embed/)[a-zA-Z0-9_-]+', raw_video) if keystring_matcher is None: keystring_matcher = re.search('' return result - + # TODO move to a more general util? Is there a better way to do the isinstance model check? class CourseSettingsEncoder(json.JSONEncoder): diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index 9cfa18c8c9..3d0b8f78af 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -6,55 +6,55 @@ from util import converters class CourseGradingModel(object): """ - Basically a DAO and Model combo for CRUD operations pertaining to grading policy. + Basically a DAO and Model combo for CRUD operations pertaining to grading policy. """ def __init__(self, course_descriptor): self.course_location = course_descriptor.location - self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100] + self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100] self.grade_cutoffs = course_descriptor.grade_cutoffs self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor) - - @classmethod + + @classmethod def fetch(cls, course_location): """ Fetch the course details for the given course from persistence and return a CourseDetails model. """ if not isinstance(course_location, Location): course_location = Location(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) model = cls(descriptor) return model - + @staticmethod def fetch_grader(course_location, index): """ - Fetch the course's nth grader + Fetch the course's nth grader Returns an empty dict if there's no such grader. """ if not isinstance(course_location, Location): course_location = Location(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) # # ??? it would be good if these had the course_location in them so that they stand alone sufficiently # # but that would require not using CourseDescriptor's field directly. Opinions? - index = int(index) - if len(descriptor.raw_grader) > index: + index = int(index) + if len(descriptor.raw_grader) > index: return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index]) - + # return empty model else: return { - "id" : index, - "type" : "", - "min_count" : 0, - "drop_count" : 0, - "short_label" : None, - "weight" : 0 + "id": index, + "type": "", + "min_count": 0, + "drop_count": 0, + "short_label": None, + "weight": 0 } - + @staticmethod def fetch_cutoffs(course_location): """ @@ -62,7 +62,7 @@ class CourseGradingModel(object): """ if not isinstance(course_location, Location): course_location = Location(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) return descriptor.grade_cutoffs @@ -73,10 +73,10 @@ class CourseGradingModel(object): """ if not isinstance(course_location, Location): course_location = Location(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) - return {'grace_period' : CourseGradingModel.convert_set_grace_period(descriptor) } - + return {'grace_period': CourseGradingModel.convert_set_grace_period(descriptor)} + @staticmethod def update_from_json(jsondict): """ @@ -85,32 +85,32 @@ class CourseGradingModel(object): """ course_location = jsondict['course_location'] descriptor = get_modulestore(course_location).get_item(course_location) - + graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']] - + descriptor.raw_grader = graders_parsed descriptor.grade_cutoffs = jsondict['grade_cutoffs'] - + get_modulestore(course_location).update_item(course_location, descriptor.definition['data']) CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period']) - + return CourseGradingModel.fetch(course_location) - - + + @staticmethod def update_grader_from_json(course_location, grader): """ - Create or update the grader of the given type (string key) for the given course. Returns the modified + Create or update the grader of the given type (string key) for the given course. Returns the modified grader which is a full model on the client but not on the server (just a dict) """ if not isinstance(course_location, Location): course_location = Location(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) # # ??? it would be good if these had the course_location in them so that they stand alone sufficiently # # but that would require not using CourseDescriptor's field directly. Opinions? - # parse removes the id; so, grab it before parse + # parse removes the id; so, grab it before parse index = int(grader.get('id', len(descriptor.raw_grader))) grader = CourseGradingModel.parse_grader(grader) @@ -118,11 +118,11 @@ class CourseGradingModel(object): descriptor.raw_grader[index] = grader else: descriptor.raw_grader.append(grader) - + get_modulestore(course_location).update_item(course_location, descriptor.definition['data']) - + return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index]) - + @staticmethod def update_cutoffs_from_json(course_location, cutoffs): """ @@ -131,18 +131,18 @@ class CourseGradingModel(object): """ if not isinstance(course_location, Location): course_location = Location(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) descriptor.grade_cutoffs = cutoffs get_modulestore(course_location).update_item(course_location, descriptor.definition['data']) - + return cutoffs - - + + @staticmethod def update_grace_period_from_json(course_location, graceperiodjson): """ - Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a + Update the course's default grace period. Incoming dict is {hours: h, minutes: m} possibly as a grace_period entry in an enclosing dict. It is also safe to call this method with a value of None for graceperiodjson. """ @@ -155,12 +155,13 @@ class CourseGradingModel(object): if 'grace_period' in graceperiodjson: graceperiodjson = graceperiodjson['grace_period'] - grace_rep = " ".join(["%s %s" % (value, key) for (key, value) in graceperiodjson.iteritems()]) + # lms requires these to be in a fixed order + grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson) descriptor = get_modulestore(course_location).get_item(course_location) descriptor.metadata['graceperiod'] = grace_rep get_modulestore(course_location).update_metadata(course_location, descriptor.metadata) - + @staticmethod def delete_grader(course_location, index): """ @@ -168,16 +169,16 @@ class CourseGradingModel(object): """ if not isinstance(course_location, Location): course_location = Location(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) - index = int(index) + index = int(index) if index < len(descriptor.raw_grader): del descriptor.raw_grader[index] # force propagation to definition descriptor.raw_grader = descriptor.raw_grader get_modulestore(course_location).update_item(course_location, descriptor.definition['data']) - - # NOTE cannot delete cutoffs. May be useful to reset + + # NOTE cannot delete cutoffs. May be useful to reset @staticmethod def delete_cutoffs(course_location, cutoffs): """ @@ -185,13 +186,13 @@ class CourseGradingModel(object): """ if not isinstance(course_location, Location): course_location = Location(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS'] get_modulestore(course_location).update_item(course_location, descriptor.definition['data']) - + return descriptor.grade_cutoffs - + @staticmethod def delete_grace_period(course_location): """ @@ -199,28 +200,28 @@ class CourseGradingModel(object): """ if not isinstance(course_location, Location): course_location = Location(course_location) - + descriptor = get_modulestore(course_location).get_item(course_location) if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod'] get_modulestore(course_location).update_metadata(course_location, descriptor.metadata) - + @staticmethod def get_section_grader_type(location): if not isinstance(location, Location): location = Location(location) - + descriptor = get_modulestore(location).get_item(location) return { - "graderType" : descriptor.metadata.get('format', u"Not Graded"), - "location" : location, - "id" : 99 # just an arbitrary value to + "graderType": descriptor.metadata.get('format', u"Not Graded"), + "location": location, + "id": 99 # just an arbitrary value to } - + @staticmethod def update_section_grader_type(location, jsondict): if not isinstance(location, Location): location = Location(location) - + descriptor = get_modulestore(location).get_item(location) if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded": descriptor.metadata['format'] = jsondict.get('graderType') @@ -228,16 +229,16 @@ class CourseGradingModel(object): else: if 'format' in descriptor.metadata: del descriptor.metadata['format'] if 'graded' in descriptor.metadata: del descriptor.metadata['graded'] - - get_modulestore(location).update_metadata(location, descriptor.metadata) - - + + get_modulestore(location).update_metadata(location, descriptor.metadata) + + @staticmethod def convert_set_grace_period(descriptor): - # 5 hours 59 minutes 59 seconds => converted to iso format + # 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59} rawgrace = descriptor.metadata.get('graceperiod', None) if rawgrace: - parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)} + parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)} return parsedgrace else: return None @@ -245,13 +246,13 @@ class CourseGradingModel(object): 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 + "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 @staticmethod @@ -260,6 +261,6 @@ class CourseGradingModel(object): if grader['weight']: grader['weight'] *= 100 if not 'short_label' in grader: - grader['short_label'] = "" - + grader['short_label'] = "" + return grader diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py new file mode 100644 index 0000000000..d088d75665 --- /dev/null +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -0,0 +1,70 @@ +from xmodule.modulestore import Location +from contentstore.utils import get_modulestore +from xmodule.x_module import XModuleDescriptor + + +class CourseMetadata(object): + ''' + For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones. + The objects have no predefined attrs but instead are obj encodings of the editable metadata. + ''' + # __new_advanced_key__ is used by client not server; so, could argue against it being here + FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__'] + + @classmethod + def fetch(cls, course_location): + """ + Fetch the key:value editable course details for the given course from persistence and return a CourseMetadata model. + """ + if not isinstance(course_location, Location): + course_location = Location(course_location) + + course = {} + + descriptor = get_modulestore(course_location).get_item(course_location) + + for k, v in descriptor.metadata.iteritems(): + if k not in cls.FILTERED_LIST: + course[k] = v + + return course + + @classmethod + def update_from_json(cls, course_location, jsondict): + """ + Decode the json into CourseMetadata and save any changed attrs to the db. + + Ensures none of the fields are in the blacklist. + """ + descriptor = get_modulestore(course_location).get_item(course_location) + + dirty = False + + for k, v in jsondict.iteritems(): + # should it be an error if one of the filtered list items is in the payload? + if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v): + dirty = True + descriptor.metadata[k] = v + + if dirty: + get_modulestore(course_location).update_metadata(course_location, descriptor.metadata) + + # 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 + return cls.fetch(course_location) + + @classmethod + def delete_key(cls, course_location, payload): + ''' + Remove the given metadata key(s) from the course. payload can be a single key or [key..] + ''' + descriptor = get_modulestore(course_location).get_item(course_location) + + for key in payload['deleteKeys']: + if key in descriptor.metadata: + del descriptor.metadata[key] + + get_modulestore(course_location).update_metadata(course_location, descriptor.metadata) + + return cls.fetch(course_location) + \ No newline at end of file diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 5bc9b53fc4..26a8adc92c 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -1,5 +1,5 @@ """ -This config file extends the test environment configuration +This config file extends the test environment configuration so that we can run the lettuce acceptance tests. """ from .test import * @@ -21,14 +21,14 @@ DATA_DIR = COURSES_ROOT # } # } -# Set this up so that rake lms[acceptance] and running the +# Set this up so that rake lms[acceptance] and running the # harvest command both use the same (test) database # which they can flush without messing up your dev db DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ENV_ROOT / "db" / "test_mitx.db", - 'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db", + 'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db", } } diff --git a/cms/envs/aws.py b/cms/envs/aws.py index b44baacb0b..a147f84531 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -5,8 +5,22 @@ import json from .common import * from logsettings import get_logger_config +import os -############################### ALWAYS THE SAME ################################ +# specified as an environment variable. Typically this is set +# in the service's upstart script and corresponds exactly to the service name. +# Service variants apply config differences via env and auth JSON files, +# the names of which correspond to the variant. +SERVICE_VARIANT = os.environ.get('SERVICE_VARIANT', None) + +# when not variant is specified we attempt to load an unvaried +# config set. +CONFIG_PREFIX = "" + +if SERVICE_VARIANT: + CONFIG_PREFIX = SERVICE_VARIANT + "." + +############### ALWAYS THE SAME ################################ DEBUG = False TEMPLATE_DEBUG = False @@ -14,9 +28,9 @@ EMAIL_BACKEND = 'django_ses.SESBackend' SESSION_ENGINE = 'django.contrib.sessions.backends.cache' DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage' -########################### NON-SECURE ENV CONFIG ############################## +############# NON-SECURE ENV CONFIG ############################## # Things like server locations, ports, etc. -with open(ENV_ROOT / "cms.env.json") as env_file: +with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file: ENV_TOKENS = json.load(env_file) LMS_BASE = ENV_TOKENS.get('LMS_BASE') @@ -35,15 +49,12 @@ for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items(): LOGGING = get_logger_config(LOG_DIR, logging_env=ENV_TOKENS['LOGGING_ENV'], syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514), - debug=False) + debug=False, + service_variant=SERVICE_VARIANT) -with open(ENV_ROOT / "repos.json") as repos_file: - REPOS = json.load(repos_file) - - -############################## SECURE AUTH ITEMS ############################### +################ SECURE AUTH ITEMS ############################### # Secret things: passwords, access keys, etc. -with open(ENV_ROOT / "cms.auth.json") as auth_file: +with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file: AUTH_TOKENS = json.load(auth_file) AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"] diff --git a/cms/envs/common.py b/cms/envs/common.py index c047d689ce..a83f61d8f9 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -20,7 +20,6 @@ Longer TODO: """ import sys -import tempfile import os.path import os import lms.envs.common @@ -33,8 +32,8 @@ MITX_FEATURES = { 'USE_DJANGO_PIPELINE': True, 'GITHUB_PUSH': False, 'ENABLE_DISCUSSION_SERVICE': False, - 'AUTH_USE_MIT_CERTIFICATES' : False, - 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests + 'AUTH_USE_MIT_CERTIFICATES': False, + 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests } ENABLE_JASMINE = False @@ -59,7 +58,8 @@ sys.path.append(COMMON_ROOT / 'lib') ############################# WEB CONFIGURATION ############################# # This is where we stick our compiled template files. -MAKO_MODULE_DIR = tempfile.mkdtemp('mako') +from tempdir import mkdtemp_clean +MAKO_MODULE_DIR = mkdtemp_clean('mako') MAKO_TEMPLATES = {} MAKO_TEMPLATES['main'] = [ PROJECT_ROOT / 'templates', @@ -74,8 +74,8 @@ TEMPLATE_DIRS = MAKO_TEMPLATES['main'] MITX_ROOT_URL = '' -LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/login' -LOGIN_URL = MITX_ROOT_URL + '/login' +LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/signin' +LOGIN_URL = MITX_ROOT_URL + '/signin' TEMPLATE_CONTEXT_PROCESSORS = ( @@ -165,13 +165,6 @@ STATICFILES_DIRS = [ # This is how you would use the textbook images locally # ("book", ENV_ROOT / "book_images") ] -if os.path.isdir(GITHUB_REPO_ROOT): - STATICFILES_DIRS += [ - # TODO (cpennington): When courses aren't loaded from github, remove this - (course_dir, GITHUB_REPO_ROOT / course_dir) - for course_dir in os.listdir(GITHUB_REPO_ROOT) - if os.path.isdir(GITHUB_REPO_ROOT / course_dir) - ] # Locale/Internationalization TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name @@ -179,6 +172,9 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identi USE_I18N = True USE_L10N = True +# Tracking +TRACK_MAX_EVENT = 10000 + # Messages MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' @@ -229,7 +225,7 @@ PIPELINE_JS = { 'source_filenames': sorted( rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.coffee') + rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.coffee') - ) + ['js/base.js'], + ) + ['js/hesitate.js', 'js/base.js'], 'output_filename': 'js/cms-application.js', }, 'module-js': { @@ -281,8 +277,13 @@ INSTALLED_APPS = ( 'contentstore', 'auth', 'student', # misleading name due to sharing with lms + 'course_groups', # not used in cms (yet), but tests run + + # tracking + 'track', # For asset pipelining 'pipeline', 'staticfiles', + 'static_replace', ) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index e29ee62e20..3dee93a398 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -12,7 +12,7 @@ TEMPLATE_DEBUG = DEBUG LOGGING = get_logger_config(ENV_ROOT / "log", logging_env="dev", tracking_filename="tracking.log", - dev_env = True, + dev_env=True, debug=True) modulestore_options = { @@ -41,7 +41,7 @@ CONTENTSTORE = { 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', 'OPTIONS': { 'host': 'localhost', - 'db' : 'xcontent', + 'db': 'xcontent', } } diff --git a/cms/envs/dev_ike.py b/cms/envs/dev_ike.py index 5fb120854b..1ebf219d44 100644 --- a/cms/envs/dev_ike.py +++ b/cms/envs/dev_ike.py @@ -9,8 +9,6 @@ import socket MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True -MITX_FEATURES['USE_DJANGO_PIPELINE']=False # don't recompile scss - -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy - +MITX_FEATURES['USE_DJANGO_PIPELINE'] = False # don't recompile scss +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy diff --git a/cms/envs/test.py b/cms/envs/test.py index d9a2597cbb..abe03edd41 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -11,7 +11,6 @@ from .common import * import os from path import path - # Nose Test Runner INSTALLED_APPS += ('django_nose',) NOSE_ARGS = ['--with-xunit'] @@ -20,7 +19,7 @@ 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 +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,6 +27,9 @@ STATIC_ROOT = TEST_ROOT / "staticfiles" 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 + # 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 = [ COMMON_ROOT / "static", @@ -63,7 +65,7 @@ CONTENTSTORE = { 'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore', 'OPTIONS': { 'host': 'localhost', - 'db' : 'xcontent', + 'db': 'xcontent', } } @@ -72,23 +74,12 @@ DATABASES = { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ENV_ROOT / "db" / "cms.db", }, - - # The following are for testing purposes... - 'edX/toy/2012_Fall': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ENV_ROOT / "db" / "course1.db", - }, - - 'edx/full/6.002_Spring_2012': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ENV_ROOT / "db" / "course2.db", - } } LMS_BASE = "localhost:8000" CACHES = { - # This is the cache used for most things. Askbot will not work without a + # This is the cache used for most things. Askbot will not work without a # functioning cache -- it relies on caching to load its settings in places. # In staging/prod envs, the sessions also live here. 'default': { @@ -115,4 +106,4 @@ CACHES = { PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher', -) \ No newline at end of file +) diff --git a/cms/manage.py b/cms/manage.py index f8773c0641..723fa59da1 100644 --- a/cms/manage.py +++ b/cms/manage.py @@ -2,7 +2,7 @@ from django.core.management import execute_manager import imp try: - imp.find_module('settings') # Assumed to be in the same directory. + imp.find_module('settings') # Assumed to be in the same directory. except ImportError: import sys sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. " diff --git a/cms/static/client_templates/advanced_entry.html b/cms/static/client_templates/advanced_entry.html new file mode 100644 index 0000000000..0312fdd344 --- /dev/null +++ b/cms/static/client_templates/advanced_entry.html @@ -0,0 +1,16 @@ +
        3. +
          + + + Keys are case sensitive and cannot contain spaces or start with a number +
          + +
          + + +
          + +
          + Delete +
          +
        4. \ No newline at end of file diff --git a/cms/static/client_templates/course_grade_policy.html b/cms/static/client_templates/course_grade_policy.html index c9a21280dd..db129614f6 100644 --- a/cms/static/client_templates/course_grade_policy.html +++ b/cms/static/client_templates/course_grade_policy.html @@ -1,69 +1,37 @@ -
        5. -
          - +
        6. +
          + + + e.g. Homework, Midterm Exams +
          -
          -
          - - e.g. Homework, Labs, Midterm Exams, Final Exam -
          -
          - - -
          - - -
          -
          - - e.g. HW, Midterm, Final -
          -
          -
          - -
          - - -
          -
          - - e.g. 25% -
          -
          -
          - -
          - - -
          -
          - - total exercises assigned -
          -
          -
          - -
          - - -
          -
          - - total exercises that won't be graded -
          -
          -
          - Delete +
          + + + e.g. HW, Midterm +
          + +
          + + + e.g. 25% +
          + +
          + + + total exercises assigned +
          + +
          + + + total exercises that won't be graded +
          + +
          + Delete +
        7. diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json index b396bec944..2249813b04 100644 --- a/cms/static/coffee/files.json +++ b/cms/static/coffee/files.json @@ -1,6 +1,10 @@ { "js_files": [ + "/static/js/vendor/RequireJS.js", "/static/js/vendor/jquery.min.js", + "/static/js/vendor/jquery-ui.min.js", + "/static/js/vendor/jquery.ui.draggable.js", + "/static/js/vendor/jquery.cookie.js", "/static/js/vendor/json2.js", "/static/js/vendor/underscore-min.js", "/static/js/vendor/backbone-min.js" diff --git a/cms/static/coffee/src/views/tabs.coffee b/cms/static/coffee/src/views/tabs.coffee index 1fbc6ffa7f..9fbe4e5789 100644 --- a/cms/static/coffee/src/views/tabs.coffee +++ b/cms/static/coffee/src/views/tabs.coffee @@ -1,6 +1,4 @@ class CMS.Views.TabsEdit extends Backbone.View - events: - 'click .new-tab': 'addNewTab' initialize: => @$('.component').each((idx, element) => @@ -13,9 +11,10 @@ class CMS.Views.TabsEdit extends Backbone.View ) ) + @options.mast.find('.new-tab').on('click', @addNewTab) @$('.components').sortable( handle: '.drag-handle' - update: (event, ui) => alert 'not yet implemented!' + update: @tabMoved helper: 'clone' opacity: '0.5' placeholder: 'component-placeholder' @@ -24,6 +23,20 @@ class CMS.Views.TabsEdit extends Backbone.View items: '> .component' ) + tabMoved: (event, ui) => + tabs = [] + @$('.component').each((idx, element) => + tabs.push($(element).data('id')) + ) + $.ajax({ + type:'POST', + url: '/reorder_static_tabs', + data: JSON.stringify({ + tabs : tabs + }), + contentType: 'application/json' + }) + addNewTab: (event) => event.preventDefault() diff --git a/cms/static/css/tiny-mce.css b/cms/static/css/tiny-mce.css index 63d2bada94..8c69c4af75 100644 --- a/cms/static/css/tiny-mce.css +++ b/cms/static/css/tiny-mce.css @@ -1,89 +1,140 @@ +@font-face{font-family:'Open Sans';font-style:normal;font-weight:700;src:local("Open Sans Bold"),local("OpenSans-Bold"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:normal;font-weight:300;src:local("Open Sans Light"),local("OpenSans-Light"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/DXI1ORHCpsQm3Vp6mXoaTaRDOzjiPcYnFooOUGCOsRk.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:italic;font-weight:700;src:local("Open Sans Bold Italic"),local("OpenSans-BoldItalic"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxhbnBKKEOwRKgsHDreGcocg.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:italic;font-weight:300;src:local("Open Sans Light Italic"),local("OpenSansLight-Italic"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxvR_54zmj3SbGZQh3vCOwvY.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:italic;font-weight:400;src:local("Open Sans Italic"),local("OpenSans-Italic"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/xjAJXh38I15wypJXxuGMBrrIa-7acMAeDBVuclsi6Gc.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;src:local("Open Sans"),local("OpenSans"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format("woff")} + .mceContentBody { - padding: 10px; - background-color: #fff; - font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif; - font-size: 16px; - line-height: 1.6; - color: #3c3c3c; - scrollbar-3dlight-color: #F0F0EE; - scrollbar-arrow-color: #676662; - scrollbar-base-color: #F0F0EE; - scrollbar-darkshadow-color: #DDDDDD; - scrollbar-face-color: #E0E0DD; - scrollbar-highlight-color: #F0F0EE; - scrollbar-shadow-color: #F0F0EE; - scrollbar-track-color: #F5F5F5; + padding: 10px; + background-color: #fff; + font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif; + font-size: 16px; + line-height: 1.6; + color: #3c3c3c; + scrollbar-3dlight-color: #F0F0EE; + scrollbar-arrow-color: #676662; + scrollbar-base-color: #F0F0EE; + scrollbar-darkshadow-color: #DDDDDD; + scrollbar-face-color: #E0E0DD; + scrollbar-highlight-color: #F0F0EE; + scrollbar-shadow-color: #F0F0EE; + scrollbar-track-color: #F5F5F5; } h1 { - color: #3c3c3c; - font-weight: normal; - font-size: 2em; - line-height: 1.4em; - letter-spacing: 1px; + color: #3c3c3c; + font-weight: normal; + font-size: 2em; + line-height: 1.4em; + letter-spacing: 1px; + margin: 0 0 1.416em 0; } h2 { - color: #646464; - font-weight: normal; - font-size: 1.2em; - line-height: 1.2em; - letter-spacing: 1px; - margin-bottom: 15px; - text-transform: uppercase; - -webkit-font-smoothing: antialiased; + color: #646464; + font-weight: normal; + font-size: 1.2em; + line-height: 1.2em; + letter-spacing: 1px; + margin-bottom: 15px; + text-transform: uppercase; + -webkit-font-smoothing: antialiased; +} + +h3, h4, h5, h6 { + margin: 0 0 10px 0; + font-weight: 600; } h3 { - font-size: 1.2em; - font-weight: 600; + font-size: 1.2em; +} + +h4 { + font-size: 1em; +} + +h5 { + font-size: .83em; +} + +h6 { + font-size: 0.75em; } p { - margin-bottom: 1.416em; - font-size: 1em; - line-height: 1.6em !important; - color: $baseFontColor; + margin-bottom: 1.416em; + font-size: 1em; + line-height: 1.6em !important; + color: #3c3c3c; } em, i { - font-style: italic; + font-style: italic; } strong, b { - font-style: bold; + font-style: bold; } p + p, ul + p, ol + p { - margin-top: 20px; + margin-top: 20px; } ol, ul { - margin: 1em 0; - padding: 0 0 0 1em; + margin: 1em 0; + padding: 0 0 0 1em; + color: #3c3c3c; + } ol li, ul li { - margin-bottom: 0.708em; + margin-bottom: 0.708em; } ol { - list-style: decimal outside none; + list-style: decimal outside none; } ul { - list-style: disc outside none; + list-style: disc outside none; } a, a:link, a:visited, a:hover, a:active { - color: #1d9dd9; -} + color: #1d9dd9; +} img { - max-width: 100%; + max-width: 100%; +} + +pre { + margin: 1em 0; + color: #3c3c3c; + font-family: monospace, serif; + font-size: 1em; + white-space: pre-wrap; + word-wrap: break-word; } code { - font-family: monospace, serif; - background: none; -} \ No newline at end of file + font-family: monospace, serif; + background: none; + color: #3c3c3c; + padding: 0; +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 16px; +} + +th { + background: #eee; + font-weight: bold; +} + +table td, th { + margin: 20px 0; + padding: 10px; + border: 1px solid #ccc !important; + text-align: left; + font-size: 14px; +} diff --git a/cms/static/img/hiw-feature1.png b/cms/static/img/hiw-feature1.png new file mode 100644 index 0000000000..3cfd48d066 Binary files /dev/null and b/cms/static/img/hiw-feature1.png differ diff --git a/cms/static/img/hiw-feature2.png b/cms/static/img/hiw-feature2.png new file mode 100644 index 0000000000..9442325dd5 Binary files /dev/null and b/cms/static/img/hiw-feature2.png differ diff --git a/cms/static/img/hiw-feature3.png b/cms/static/img/hiw-feature3.png new file mode 100644 index 0000000000..fa6b81ae89 Binary files /dev/null and b/cms/static/img/hiw-feature3.png differ diff --git a/cms/static/img/html-icon.png b/cms/static/img/html-icon.png index e739f2fc11..8f576178b2 100644 Binary files a/cms/static/img/html-icon.png and b/cms/static/img/html-icon.png differ diff --git a/cms/static/img/large-advanced-icon.png b/cms/static/img/large-advanced-icon.png new file mode 100644 index 0000000000..c6a19ea5a9 Binary files /dev/null and b/cms/static/img/large-advanced-icon.png differ diff --git a/cms/static/img/large-annotations-icon.png b/cms/static/img/large-annotations-icon.png new file mode 100644 index 0000000000..249193521f Binary files /dev/null and b/cms/static/img/large-annotations-icon.png differ diff --git a/cms/static/img/large-discussion-icon.png b/cms/static/img/large-discussion-icon.png index 2f0bfea98f..cebf332769 100644 Binary files a/cms/static/img/large-discussion-icon.png and b/cms/static/img/large-discussion-icon.png differ diff --git a/cms/static/img/large-freeform-icon.png b/cms/static/img/large-freeform-icon.png index b1d195a7ca..0d5e454f58 100644 Binary files a/cms/static/img/large-freeform-icon.png and b/cms/static/img/large-freeform-icon.png differ diff --git a/cms/static/img/large-openended-icon.png b/cms/static/img/large-openended-icon.png new file mode 100644 index 0000000000..4d31815413 Binary files /dev/null and b/cms/static/img/large-openended-icon.png differ diff --git a/cms/static/img/large-problem-icon.png b/cms/static/img/large-problem-icon.png index b962d42b14..a30ab8eac8 100644 Binary files a/cms/static/img/large-problem-icon.png and b/cms/static/img/large-problem-icon.png differ diff --git a/cms/static/img/large-video-icon.png b/cms/static/img/large-video-icon.png index 392851324c..f1ab048b4c 100644 Binary files a/cms/static/img/large-video-icon.png and b/cms/static/img/large-video-icon.png differ diff --git a/cms/static/img/logo-edx-studio-white.png b/cms/static/img/logo-edx-studio-white.png new file mode 100644 index 0000000000..3e3ee63622 Binary files /dev/null and b/cms/static/img/logo-edx-studio-white.png differ diff --git a/cms/static/img/logo-edx-studio.png b/cms/static/img/logo-edx-studio.png new file mode 100644 index 0000000000..006194a195 Binary files /dev/null and b/cms/static/img/logo-edx-studio.png differ diff --git a/cms/static/img/pl-1x1-000.png b/cms/static/img/pl-1x1-000.png new file mode 100644 index 0000000000..b94b7a9746 Binary files /dev/null and b/cms/static/img/pl-1x1-000.png differ diff --git a/cms/static/img/pl-1x1-fff.png b/cms/static/img/pl-1x1-fff.png new file mode 100644 index 0000000000..7081c75d36 Binary files /dev/null and b/cms/static/img/pl-1x1-fff.png differ diff --git a/cms/static/img/preview-lms-staticpages.png b/cms/static/img/preview-lms-staticpages.png new file mode 100644 index 0000000000..05a62f7c7f Binary files /dev/null and b/cms/static/img/preview-lms-staticpages.png differ diff --git a/cms/static/img/thumb-hiw-feature1.png b/cms/static/img/thumb-hiw-feature1.png new file mode 100644 index 0000000000..b2dc0c00ee Binary files /dev/null and b/cms/static/img/thumb-hiw-feature1.png differ diff --git a/cms/static/img/thumb-hiw-feature2.png b/cms/static/img/thumb-hiw-feature2.png new file mode 100644 index 0000000000..e96bcad1aa Binary files /dev/null and b/cms/static/img/thumb-hiw-feature2.png differ diff --git a/cms/static/img/thumb-hiw-feature3.png b/cms/static/img/thumb-hiw-feature3.png new file mode 100644 index 0000000000..f694fca516 Binary files /dev/null and b/cms/static/img/thumb-hiw-feature3.png differ diff --git a/cms/static/js/base.js b/cms/static/js/base.js index e99fc9a4da..d8b32cb0e8 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -5,7 +5,7 @@ var $newComponentItem; var $changedInput; var $spinner; -$(document).ready(function() { +$(document).ready(function () { $body = $('body'); $modal = $('.history-modal'); $modalCover = $(' diff --git a/cms/templates/activation_complete.html b/cms/templates/activation_complete.html index 5d9437ccb3..1e195a632c 100644 --- a/cms/templates/activation_complete.html +++ b/cms/templates/activation_complete.html @@ -5,7 +5,7 @@

          Activation Complete!

          -

          Thanks for activating your account. Log in here.

          +

          Thanks for activating your account. Log in here.

          diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index 01766e2dac..a5a9144b07 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -1,7 +1,7 @@ <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> -<%block name="bodyclass">assets -<%block name="title">Courseware Assets +<%block name="bodyclass">is-signedin course uploads +<%block name="title">Uploads & Files <%namespace name='static' file='static_content.html'/> @@ -28,17 +28,32 @@ {{uploadDate}} - + +
          +
          +
          + Course Content +

          Files & Uploads

          +
          + + +
          +
          +
          @@ -69,7 +84,7 @@ ${asset['uploadDate']} - + % endfor @@ -100,7 +115,7 @@
          - +
          diff --git a/cms/templates/base.html b/cms/templates/base.html index 84f10fc2d1..498897bd11 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -5,23 +5,29 @@ + + <%block name="title"></%block> | + % if context_course: + <% ctx_loc = context_course.location %> + ${context_course.display_name} | + % endif + edX Studio + + + + <%static:css group='base-style'/> - + - <%block name="title"></%block> - - - - <%block name="header_extras"> - <%include file="widgets/header.html" args="active_tab=active_tab"/> + <%include file="widgets/header.html" /> <%include file="courseware_vendor_js.html"/> @@ -47,9 +53,9 @@ <%block name="content"> + <%include file="widgets/footer.html" /> <%block name="jsextra"> - diff --git a/cms/templates/course_index.html b/cms/templates/course_index.html index e490ad7817..5c8772c1ed 100644 --- a/cms/templates/course_index.html +++ b/cms/templates/course_index.html @@ -1,5 +1,5 @@ <%inherit file="base.html" /> -<%block name="title">Course Manager + <%include file="widgets/header.html"/> <%block name="content"> diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index f4fa661b6e..55dcaaa068 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -2,14 +2,16 @@ <%namespace name='static' file='static_content.html'/> -<%block name="title">Course Info -<%block name="bodyclass">course-info +<%block name="title">Updates +<%block name="bodyclass">is-signedin course course-info updates + <%block name="jsextra"> + @@ -18,8 +20,8 @@ <%block name="content"> +
          +
          +
          + Course Content +

          Static Pages

          +
          + + +
          +
          + +
          +
          + +
          +
          +
          -
          -

          Here you can add and manage additional pages for your course

          -

          These pages will be added to the primary navigation menu alongside Courseware, Course Info, Discussion, etc.

          -
          - -
            @@ -43,4 +67,17 @@
          + +
          +

          How Static Pages are Used in Your Course

          +
          + Preview of how Static Pages are used in your course +
          These pages will be presented in your course's main navigation alongside Courseware, Course Info, Discussion, etc.
          +
          + + + + close modal + +
          \ No newline at end of file diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index c334724121..43775122d4 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -7,8 +7,9 @@ %> <%! from django.core.urlresolvers import reverse %> -<%block name="bodyclass">subsection <%block name="title">CMS Subsection +<%block name="bodyclass">is-signedin course subsection + <%namespace name="units" file="widgets/units.html" /> <%namespace name='static' file='static_content.html'/> @@ -21,11 +22,7 @@
          - -
          -
          - - +
          @@ -48,7 +45,7 @@ <%block name="jsextra"> @@ -111,6 +109,8 @@ + + - - + \ No newline at end of file diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 36930f5386..722e756203 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -1,17 +1,31 @@ <%inherit file="base.html" /> <%block name="title">Course Staff Manager -<%block name="bodyclass">users +<%block name="bodyclass">is-signedin course users settings team + <%block name="content"> +
          +
          +
          + Course Settings +

          Course Team

          +
          + + +
          +
          +
          -
          - %if allow_actions: - - New User - - %endif -

          The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional course staff below, if you are the course instructor. Please note that they must have already registered and verified their account.

          @@ -97,7 +111,7 @@ $cancelButton.bind('click', hideNewUserForm); $('.new-user-button').bind('click', showNewUserForm); - $body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel); + $('body').bind('keyup', { $cancelButton: $cancelButton }, checkForCancel); $('.remove-user').click(function() { $.ajax({ diff --git a/cms/templates/overview.html b/cms/templates/overview.html index a20531200e..1098a6885e 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -6,7 +6,8 @@ from datetime import datetime %> <%! from django.core.urlresolvers import reverse %> -<%block name="title">CMS Courseware Overview +<%block name="title">Course Outline +<%block name="bodyclass">is-signedin course outline <%namespace name='static' file='static_content.html'/> <%namespace name="units" file="widgets/units.html" /> @@ -18,6 +19,7 @@ + + - - - + + <%block name="content"> - -
          -
          -

          Settings

          -
          - -
          +
          +
          +
          + Settings +

          Schedule & Details

          +
          +
          +
          -
          -

          Course Details

          - -
          +
          +
          +
          + +
          -

          Basic Information

          - The nuts and bolts of your course +

          Basic Information

          + The nuts and bolts of your course
          -
          - -
          -
          - - This is used in your course URL, and cannot be changed -
          -
          -
          +
            +
          1. + + +
          2. -
            - -
            -
            - - This is used in your course URL, and cannot be changed -
            -
            -
            +
          3. + + +
          4. -
            - -
            -
            - - This is used in your course URL, and cannot be changed -
            -
            -
            -
          +
        8. + + +
        9. +

        + These are used in your course URL, and cannot be changed +
        -
        +
        -

        Course Schedule

        - Important steps and segments of your course +

        Course Schedule

        + Important steps and segments of your course
        -
        -

        Course Dates:

        - -
        -
        -
        - - +
          +
        1. +
          + + First day the course begins
          -
          - - +
          + +
          -
          -
        +

      2. -
        -
        -
        - - - Last day the course is active +
      3. +
        + + + Last day your course is active
        -
        - - +
        + +
        -
        -
      4. -
        + +
      -
      -

      Enrollment Dates:

      - -
      -
      -
      - - +
        +
      1. +
        + + First day students can enroll
        -
        - - +
        + +
        -
        -
      + -
      -
      -
      - - +
    4. +
      + + Last day students can enroll
      -
      - - +
      + +
      -
      -
    5. -
      - - - + + +
      -
      -
      -

      Introducing Your Course

      - Information for perspective students -
      +
      +
      +

      Introducing Your Course

      + Information for prospective students +
      -
      - -
      -
      - - Introductions, prerequisites, FAQs that are used on your course summary page -
      -
      -
      +
        +
      1. + + + Introductions, prerequisites, FAQs that are used on your course summary page +
      2. -
        - -
        +
      3. +
        - - Video restrictions go here + + Enter your YouTube video's ID (along with any restriction parameters)
        -
      4. -
        -
      + + +

      -
      +
      -

      Requirements

      - Expectations of the students taking this course +

      Requirements

      + Expectations of the students taking this course
      -
      - -
      -
      - +
        +
      1. + + Time spent on all course work -
      -
      -
      -
      -
      - -
      -

      Faculty

      - -
      -
      -

      Faculty Members

      - Individuals instructing and help with this course -
      - -
      -
      -
        -
      • -
        - -
        - -
        -
        - -
        - -
        - -
        -
        - -
        - - -
        - -
        - -
        - - A brief description of your education, experience, and expertise -
        -
        - - Delete Faculty Member
      • - -
      • -
        - -
        - -
        -
        - -
        - -
        - -
        -
        - -
        - -
        -
        - - Upload Faculty Photo - - Max size: 30KB -
        -
        -
        - -
        - -
        -
        - - A brief description of your education, experience, and expertise -
        -
        -
        -
      • -
      - - - New Faculty Member - -
      -
      -
      - -
      - -
      -

      Grading

      - -
      -
      -

      Overall Grade Range

      - Course grade ranges and their values -
      - -
      - -
      - -
      -
      -
        -
      1. 0
      2. -
      3. 10
      4. -
      5. 20
      6. -
      7. 30
      8. -
      9. 40
      10. -
      11. 50
      12. -
      13. 60
      14. -
      15. 70
      16. -
      17. 80
      18. -
      19. 90
      20. -
      21. 100
      -
        -
      -
      -
      -
      - -
      + + -
      -
      -

      General Grading

      - Deadlines and Requirements -
      +
      - -
      -
      -

      Assignment Types

      -
      - - -
      -
      - -
      -

      Problems

      - -
      -
      -

      General Settings

      - Course-wide settings for all problems -
      - -
      -

      Problem Randomization:

      - -
      -
      - - -
      - - randomize all problems -
      -
      - -
      - - -
      - - do not randomize problems -
      -
      - -
      - - -
      - - randomize problems per student -
      -
      -
      -
      - -
      -

      Show Answers:

      - -
      -
      - - -
      - - Answers will be shown after the number of attempts has been met -
      -
      - -
      - - -
      - - Answers will never be shown, regardless of attempts -
      -
      -
      -
      - -
      - - -
      -
      - - Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0" -
      -
      -
      -
      - -
      -
      -

      [Assignment Type Name]

      -
      - -
      -

      Problem Randomization:

      - -
      -
      - - -
      - - randomize all problems -
      -
      - -
      - - -
      - - do not randomize problems -
      +

      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.

      -
      - - -
      - - randomize problems per student -
      -
      -
      -
      - -
      -

      Show Answers:

      - -
      -
      - - -
      - - Answers will be shown after the number of attempts has been met -
      -
      - -
      - - -
      - - Answers will never be shown, regardless of attempts -
      -
      -
      -
      - -
      - - -
      -
      - - Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0" -
      -
      -
      -
      -
      - -
      -

      Discussions

      - -
      -
      -

      General Settings

      - Course-wide settings for online discussion -
      - -
      -

      Anonymous Discussions:

      - -
      -
      - - -
      - - Students and faculty will be able to post anonymously -
      -
      - -
      - - -
      - - Posting anonymously is not allowed. Any previous anonymous posts will be reverted to non-anonymous -
      -
      -
      -
      - -
      -

      Anonymous Discussions:

      - -
      -
      - - -
      - - Students and faculty will be able to post anonymously -
      -
      - -
      - - -
      - - This option is disabled since there are previous discussions that are anonymous. -
      -
      -
      -
      - -
      -

      Discussion Categories

      - -
      - - - - New Discussion Category - -
      -
      -
      -
      -
      - -
      -
      -
      - +
      + % if context_course: + <% ctx_loc = context_course.location %> + <%! from django.core.urlresolvers import reverse %> +

      Other Course Settings

      + + % endif +
      + + +
      + \ No newline at end of file diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html new file mode 100644 index 0000000000..ceee406398 --- /dev/null +++ b/cms/templates/settings_advanced.html @@ -0,0 +1,124 @@ +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> +<%block name="title">Advanced Settings +<%block name="bodyclass">is-signedin course advanced settings + +<%namespace name='static' file='static_content.html'/> +<%! +from contentstore import utils +%> + +<%block name="jsextra"> + + + + + + + + + + +<%block name="content"> +
      +
      +
      + Settings +

      Advanced Settings

      +
      + +
      +
      + +
      + Your policy changes have been saved. +
      + +
      + There was an error saving your information. Please see below. +
      + +
      +
      +

      Manual Policy Definition

      + Manually Edit Course Policy Values (JSON Key / Value pairs) +
      + +

      Warning: Add only manual policy data that you are familiar + with.

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

      Note: Your changes will not take effect until you save your + progress. Take care with key and value formatting, as validation is not implemented.

      +
      + +
      + +
      +
      +
      + \ No newline at end of file diff --git a/cms/templates/settings_discussions_faculty.html b/cms/templates/settings_discussions_faculty.html new file mode 100644 index 0000000000..fc30b6eebb --- /dev/null +++ b/cms/templates/settings_discussions_faculty.html @@ -0,0 +1,430 @@ + +<%inherit file="base.html" /> +<%block name="title">Schedule and details +<%block name="bodyclass">is-signedin course settings + + +<%namespace name='static' file='static_content.html'/> +<%! +from contentstore import utils +%> + + +<%block name="jsextra"> + + + + + + + + +<%block name="content"> + +
      +
      +

      Settings

      +
      +
      + +
      +

      Faculty

      + +
      +
      +

      Faculty Members

      + Individuals instructing and help with this course +
      + +
      +
      +
        +
      • +
        + +
        + +
        +
        + +
        + +
        + +
        +
        + +
        + + +
        + +
        + +
        + + A brief description of your education, experience, and expertise +
        +
        + + Delete Faculty Member +
      • + +
      • +
        + +
        + +
        +
        + +
        + +
        + +
        +
        + +
        + +
        +
        + + Upload Faculty Photo + + Max size: 30KB +
        +
        +
        + +
        + +
        +
        + + A brief description of your education, experience, and expertise +
        +
        +
        +
      • +
      + + + New Faculty Member + +
      +
      +
      + +
      + +
      +

      Problems

      + +
      +
      +

      General Settings

      + Course-wide settings for all problems +
      + +
      +

      Problem Randomization:

      + +
      +
      + + +
      + + randomize all problems +
      +
      + +
      + + +
      + + do not randomize problems +
      +
      + +
      + + +
      + + randomize problems per student +
      +
      +
      +
      + +
      +

      Show Answers:

      + +
      +
      + + +
      + + Answers will be shown after the number of attempts has been met +
      +
      + +
      + + +
      + + Answers will never be shown, regardless of attempts +
      +
      +
      +
      + +
      + + +
      +
      + + Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0" +
      +
      +
      +
      + +
      +
      +

      [Assignment Type Name]

      +
      + +
      +

      Problem Randomization:

      + +
      +
      + + +
      + + randomize all problems +
      +
      + +
      + + +
      + + do not randomize problems +
      +
      + +
      + + +
      + + randomize problems per student +
      +
      +
      +
      + +
      +

      Show Answers:

      + +
      +
      + + +
      + + Answers will be shown after the number of attempts has been met +
      +
      + +
      + + +
      + + Answers will never be shown, regardless of attempts +
      +
      +
      +
      + +
      + + +
      +
      + + Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0" +
      +
      +
      +
      +
      + +
      +

      Discussions

      + +
      +
      +

      General Settings

      + Course-wide settings for online discussion +
      + +
      +

      Anonymous Discussions:

      + +
      +
      + + +
      + + Students and faculty will be able to post anonymously +
      +
      + +
      + + +
      + + Posting anonymously is not allowed. Any previous anonymous posts will be reverted to non-anonymous +
      +
      +
      +
      + +
      +

      Anonymous Discussions:

      + +
      +
      + + +
      + + Students and faculty will be able to post anonymously +
      +
      + +
      + + +
      + + This option is disabled since there are previous discussions that are anonymous. +
      +
      +
      +
      + +
      +

      Discussion Categories

      + +
      + + + + New Discussion Category + +
      +
      +
      +
      +
      +
      +
      +
      +
      + diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html new file mode 100644 index 0000000000..923cc35313 --- /dev/null +++ b/cms/templates/settings_graders.html @@ -0,0 +1,152 @@ +<%inherit file="base.html" /> +<%block name="title">Grading +<%block name="bodyclass">is-signedin course grading settings + +<%namespace name='static' file='static_content.html'/> +<%! +from contentstore import utils +%> + +<%block name="jsextra"> + + + + + + + + + + + + + +<%block name="content"> +
      +
      +
      + Settings +

      Grading

      +
      +
      +
      + +
      +
      +
      +
      +
      +
      +

      Overall Grade Range

      + Your overall grading scale for student final grades +
      + +
        +
      1. +
        + +
        +
        +
          +
        1. 0
        2. +
        3. 10
        4. +
        5. 20
        6. +
        7. 30
        8. +
        9. 40
        10. +
        11. 50
        12. +
        13. 60
        14. +
        15. 70
        16. +
        17. 80
        18. +
        19. 90
        20. +
        21. 100
        22. +
        +
          +
        +
        +
        +
        +
      2. +
      +
      + +
      + +
      +
      +

      Grading Rules & Policies

      + Deadlines, requirements, and logistics around grading student work +
      + +
        +
      1. + + + Leeway on due dates +
      2. +
      +
      + +
      + +
      +
      +

      Assignment Types

      + Categories and labels for any exercises that are gradable +
      + +
        + +
      + + +
      +
      +
      + + +
      +
      + diff --git a/cms/templates/signup.html b/cms/templates/signup.html index 2c60b758e6..30c5c1cf2b 100644 --- a/cms/templates/signup.html +++ b/cms/templates/signup.html @@ -1,94 +1,141 @@ <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> -<%block name="title">Sign up -<%block name="bodyclass">no-header +<%block name="title">Sign Up +<%block name="bodyclass">not-signedin signup <%block name="content"> -
      +
      +
      +
      +

      Sign Up for edX Studio

      + +
      - +
      +

      I've never authored a course online before. Is there help?

      +

      Absolutely. We have created an online course, edX101, that describes some best practices: from filming video, creating exercises, to the basics of running an online course. Additionally, we're always here to help, just drop us a note.

      +
      + +
      +
      + - + ); + }); + })(this) + \ No newline at end of file diff --git a/cms/templates/unit.html b/cms/templates/unit.html index ef94d51576..5cd872ce98 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -1,8 +1,9 @@ <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> <%namespace name="units" file="widgets/units.html" /> -<%block name="bodyclass">unit -<%block name="title">CMS Unit +<%block name="title">Individual Unit +<%block name="bodyclass">is-signedin course unit + <%block name="jsextra"> @@ -35,7 +44,7 @@
      -

      +

        % for id in components:
      1. @@ -56,38 +65,66 @@
      % for type, templates in sorted(component_templates.items()):
      -

      Select ${type} component type:

      - - + % if type == "problem": +
      + + % endif +
      +
        + % for name, location, has_markdown, is_empty in templates: + % if has_markdown or type != "problem": + % if is_empty: +
      • + + ${name} + +
      • + + % else: +
      • + + ${name} + +
      • + % endif + % endif + + %endfor +
      +
      + % if type == "problem": +
      +
        + % for name, location, has_markdown, is_empty in templates: + % if not has_markdown: + % if is_empty: +
      • + + ${name} + +
      • + + % else: +
      • + + ${name} + + +
      • + % endif + % endif + % endfor +
      +
      +
      + % endif Cancel
      % endfor @@ -98,7 +135,7 @@ - -
      -

      Getting Help

      +
      +

      Getting help.

      +
      +

      You have a vibrant, global community of fellow online learners available 24-7 to help with the course within the framework of the Honor Code, as well as support from the TAs who monitor the course. Take a look at the course’s Discussion Forum where you can review questions, answers and comments from fellow online learners, as well as post a question.

      +
      +

      Can I re-take a course?

      -
      +

      Good news: there are unlimited "mulligans" in edX. You may re-take edX courses as often as you wish. Your performance in any particular offering of a course will not effect your standing in future offerings of any edX course, including future offerings of the same course.

      Enrollment for a course that I am interested in is open, but the course has already started. Can I still enroll?

      -
      +

      Yes, but you will not be able to turn in any assignments or exams that have already been due. If it is early in the course, you might still be able to earn enough points for a certificate, but you will have to check with the course in question in order to find out more.

      Is there an exam at the end?

      -
      +

      Different courses have slightly different structures. Please check the course material description to see if there is a final exam or final project.

      Will the same courses be offered again in the future?

      -
      +

      Existing edX courses will be re-offered, and more courses added.

      +
      + + +
      +

      Certificates & Credits

      +

      Will I get a certificate for taking an edX course?

      -
      -

      Online learners who receive a passing grade for a course will receive a certificate of mastery from edX and the underlying X University that offered the course. For example, a certificate of mastery for MITx’s 6.002x Circuits & Electronics will come from edX and MITx.

      +
      +

      Online learners who receive a passing grade for a course will receive a certificate + of mastery at the discretion of edX and the underlying X University that offered + the course. For example, a certificate of mastery for MITx’s 6.002x Circuits & + Electronics will come from edX and MITx.

      +

      If you passed the course, your certificate of mastery will be delivered online + through edx.org. So be sure to check your email in the weeks following the final + grading – you will be able to download and print your certificate. Note: At this + time, edX is holding certificates for learners connected with Cuba, Iran, Syria + and Sudan pending confirmation that the issuance is in compliance with U.S. + embargoes.

      How are edX certificates delivered?

      -
      +

      EdX certificates are delivered online through edx.org. So be sure to check your email in the weeks following the final grading – you will be able to download and print your certificate.

      What is the difference between a proctored certificate and an honor code certificate?

      -
      +

      A proctored certificate is given to students who take and pass an exam under proctored conditions. An honor-code certificate is given to students who have completed all of the necessary online coursework associated with a course and have signed the edX honor code .

      -

      Yes. The requirements for both certificates can be independently satisfied.

      -
      -

      It is certainly possible to pass an edX course if you miss a week; however, coursework is progressive, so you should review and study what you may have missed. You can check your progress dashboard in the course to see your course average along the way if you have any concerns.

      +

      Can I get both a proctored certificate and an honor code certificate?

      +
      +

      Yes. The requirements for both certificates can be independently satisfied.

      Will my grade be shown on my certificate?

      -
      +

      No. Grades are not displayed on either honor code or proctored certificates.

      How can I talk to professors, fellows and teaching assistants?

      -
      +

      The Discussion Forums are the best place to reach out to the edX teaching team for your class, and you don’t have to wait in line or rearrange your schedule to fit your professor’s – just post your questions. The response isn’t always immediate, but it’s usually pretty darned quick.

      The only certificates distributed with grades by edX were for the initial prototype course.

      -
      +

      You may unregister from an edX course at anytime, there are absolutely no penalties associated with incomplete edX studies, and you may register for the same course (provided we are still offering it) at a later time.

      Will my university accept my edX coursework for credit?

      -
      +

      Each educational institution makes its own decision regarding whether to accept edX coursework for credit. Check with your university for its policy.

      I lost my edX certificate – can you resend it to me?

      -
      +

      Please log back in to your account to find certificates from the same profile page where they were originally posted. You will be able to re-print your certificate from there.

      @@ -286,12 +298,25 @@

      There’s no need to delete you account. An old, unused edX account with no course completions associated with it will disappear.

      +
      +

      I made a mistake creating my username – how do I fix it?

      +
      +

      In most cases it would simplest and fastest to create a new account. Your old unused account will vanish naturally. If you were not aware of your mistake until much later, you should send us a detailed change request and we will do our best to edit your username. Please bear in mind that usernames are unique, and the one you want may be taken.

      +
      +

      I am experiencing problems with the display. E.g., There are tools missing from the course display, or I am unable to view video.

      Please check your browser and settings. We recommend downloading the current version of Firefox or Chrome. Alternatively, you may re-register with a different email account. There is no need to delete the old account, as it will disappear if unused.

      +
      +

      How can I help edX?

      +
      +

      You may not realize it, but just by taking a course you are helping edX. That’s because the edX platform has been specifically designed to not only teach, but also gather data about learning. EdX will utilize this data to find out how to improve education online and on-campus.

      +
      +
      +
      @@ -299,7 +324,7 @@ diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html index f2752a0939..da31e07e42 100644 --- a/lms/templates/static_templates/jobs.html +++ b/lms/templates/static_templates/jobs.html @@ -14,17 +14,17 @@
      -

      Our mission is to transform learning.

      +

      Our mission is to transform learning.

      -
      -

      “EdX represents a unique opportunity to improve education on our campuses through online learning, while simultaneously creating a bold new educational path for millions of learners worldwide.”

      - —Rafael Reif, MIT President -
      +
      +

      “EdX represents a unique opportunity to improve education on our campuses through online learning, while simultaneously creating a bold new educational path for millions of learners worldwide.”

      + —Rafael Reif, MIT President +
      -
      -

      “EdX gives Harvard and MIT an unprecedented opportunity to dramatically extend our collective reach by conducting groundbreaking research into effective education and by extending online access to quality higher education.”

      - —Drew Faust, Harvard President -
      +
      +

      “EdX gives Harvard and MIT an unprecedented opportunity to dramatically extend our collective reach by conducting groundbreaking research into effective education and by extending online access to quality higher education.”

      + —Drew Faust, Harvard President +
      @@ -34,132 +34,428 @@
      -
      +

      EdX is looking to add new talent to our team!

      Our mission is to give a world-class education to everyone, everywhere, regardless of gender, income or social status

      Today, EdX.org, a not-for-profit provides hundreds of thousands of people from around the globe with access to free education.  We offer amazing quality classes by the best professors from the best schools. We enable our members to uncover a new passion that will transform their lives and their communities.

      -

      Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you're results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.

      +

      Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you’re results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.

      As part of the edX team, you’ll receive:

      • Competitive compensation
      • Generous benefits package
      • Free lunch every day
      • -
      • A great working experience where everyone cares
      • +
      • A great working experience where everyone cares and wants to change the world (no, we’re not kidding)
      -

      While we appreciate every applicant's interest, only those under consideration will be contacted. We regret that phone calls will not be accepted.

      +

      While we appreciate every applicant’s interest, only those under consideration will be contacted. We regret that phone calls will not be accepted. Equal opportunity employer.

      +
      +
      + + + + + + +
      +
      +

      DIRECTOR OF EDUCATIONAL SERVICES

      +

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

      +
        +
      1. Delivering 20 new courses in 2013 in collaboration with the partner Universities +
          +
        • 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.
        • +
        +
      2. +
      3. 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.
        • +
        • 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.
        • +
        +
      4. +
      5. Growing the Team, Growing the Business +
          +
        • The edX Director will be responsible for working with Business Development to identify revenue opportunities and build profitable plans to grow the business and grow the team.
        • +
        • Maintain for-profit nimbleness in an organization committed to non-profit ideals.
        • +
        • Design scalable solutions to opportunities revealed by technical innovations
        • +
        +
      6. +
      7. Integrating a Strong Team within Strong Organization +
          +
        • Connect organization’s management and University leadership with consistent and high quality expectations and deployment
        • +
        • Integrate with a highly collaborative leadership team to maximize talents of the organization
        • +
        • Successfully escalate issues within and beyond the organization to ensure the best possible educational outcome for students and Universities
        • +
        +
      8. +
      +

      Skills:

      +
        +
      • Ability to lead simultaneous initiatives in an entrepreneurial culture
      • +
      • Self-starter, challenger, strategic planner, analytical thinker
      • +
      • Excellent written and verbal skills
      • +
      • Strong, proactive leadership
      • +
      • Experience with deploying educational technologies on a large scale
      • +
      • Develop team skills in a ferociously intelligent group
      • +
      • Fan the enthusiasm of the partner Universities when the enormity of the transition they are facing becomes intimidating
      • +
      • Encourage creativity to allow the technology to provoke pedagogical possibilities that brick and mortar classes have precluded.
      • +
      • Lean and Agile thinking and training. Experienced in scrum or kanban.
      • +
      • Design and deliver hiring/development plans which meet rapidly changing skill needs.
      • +
      + +

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

      +
      +
      + +
      +
      +

      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

      +

      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.
      • +
      • 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.
      • +
      • Work with program managers to develop training benchmarks and Key Performance Indicators. Monitor progress and proactively make adjustments as necessary.
      • +
      • Collaborate with product development on creating documentation and user guides.
      • +
      • Provide on-going evaluation of the effectiveness of edX training programs.
      • +
      • Assist in the revision/refinement of training curricula and resources.
      • +
      • Grow a train-the-trainer organization with edX partners, identifying expert edX users to provide on-site peer assistance.
      • +
      • Deliver internal and external trainings.
      • +
      • Coordinate with internal teams to ensure appropriate preparation for trainings, and follow-up after delivery.
      • +
      • Maintain training reporting database and training records.
      • +
      • Produce training evaluation reports, training support plans, and training improvement plans.
      • +
      • Quickly become an expert on edX’s standards, procedures and tools.
      • +
      • Stay current on emerging trends in eLearning, platform support and implementation strategy.
      • +
      +

      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.
      • +
      • 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.
      • +
      • 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 required, Master’s in Education, organizational learning, or other related field preferred.
      • +
      + +

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

      -

      INSTRUCTIONAL DESIGNER — CONTRACT OPPORTUNITY

      -

      The Instructional Designer will work collaboratively with the edX content and engineering teams to plan, develop and deliver highly engaging and media rich online courses. The Instructional Designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines.

      -

      Responsibilities:

      -
        -
      • Work with the video production team, product managers and course staff on the implementation of instructional design approaches in the development of media and other course materials.
      • -
      • Based on course staff and faculty input, articulate learning objectives and align them to design strategies and assessments.
      • -
      • Develop flipped classroom instructional strategies in coordination with community college faculty.
      • -
      • Produce clear and instructionally effective copy, instructional text, and audio and video scripts
      • -
      • Identify and deploy instructional design best practices for edX course staff and faculty as needed.
      • -
      • Create course communication style guides. Train and coach teaching staff on best practices for communication and discussion management.
      • -
      • Serve as a liaison to instructional design teams based at X universities.
      • -
      • Consult on peer review processes to be used by learners in selected courses.
      • -
      • Ability to apply game-based learning theory and design into selected courses as appropriate.
      • -
      • Use learning analytics and metrics to inform course design and revision process.
      • -
      • Collaborate with key research and learning sciences stakeholders at edX and partner institutions for the development of best practices for MOOC teaching and learning and course design.
      • -
      • Support the development of pilot courses and modules used for sponsored research initiatives.
      • -
      -

      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.
      • -
      • 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.
      • -
      -

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

      -

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

      -
      -
      - -
      -
      -

      MEMBER SERVICES MANAGER

      -

      The edX Member Services Manager is responsible for both defining support best practices and directly supporting edX members by handling or routing issues that come in from our websites, email and social media tools.  We are looking for a passionate person to help us define and own this experience. While this is a Manager level position, we see this candidate quickly moving through the ranks, leading a larger team of employees over time. This staff member will be running our fast growth support organization.

      +

      INSTRUCTIONAL DESIGNER

      +

      The Instructional Designer will work collaboratively with the edX content and engineering teams to plan, develop and deliver highly engaging and media rich online courses. The Instructional Designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines.

      Responsibilities:

        -
      • Define and rollout leading technology, best practices and policies to support a growing team of member care representatives.
      • -
      • Provide reports and visibility into member care metrics.
      • -
      • Identify a staffing plan that mirrors growth and work to grow the team with passionate, member-first focused staff.
      • -
      • Manage member services staff to predefined service levels.
      • -
      • Resolve issues according to edX policies; escalates non-routine issues.
      • -
      • Educate members on edX policies and getting started
      • -
      • May assist new members with edX procedures and processing registration issues.
      • -
      • Provides timely follow-up and resolution to issues.
      • -
      • A passion for doing the right thing - at edX the member is always our top priority
        -
      • +
      • Work with the video production team, product managers and course staff on the implementation of instructional design approaches in the development of media and other course materials.
      • +
      • Based on course staff and faculty input, articulate learning objectives and align them to design strategies and assessments.
      • +
      • Develop flipped classroom instructional strategies in coordination with community college faculty.
      • +
      • Produce clear and instructionally effective copy, instructional text, and audio and video scripts
      • +
      • Identify and deploy instructional design best practices for edX course staff and faculty as needed.
      • +
      • Create course communication style guides. Train and coach teaching staff on best practices for communication and discussion management.
      • +
      • Serve as a liaison to instructional design teams based at our partner Universities.
      • +
      • Consult on peer review processes to be used by learners in selected courses.
      • +
      • Ability to apply game-based learning theory and design into selected courses as appropriate.
      • +
      • Use learning analytics and metrics to inform course design and revision process.
      • +
      • Collaborate with key research and learning sciences stakeholders at edX and partner institutions for the development of best practices for MOOC teaching and learning and course design.
      • +
      • Support the development of pilot courses and modules used for sponsored research initiatives.

      Qualifications:

        -
      • 5-8 years in a call center or support team management
      • -
      • Exemplary customer service skills
      • -
      • Experience in creating and rolling out support/service best practices
      • -
      • Solid computer skills – must be fluent with desktop applications and have a basic understanding of web technologies (i.e. basic HTML)
      • -
      • Problem solving - the individual identifies and resolves problems in a timely manner, gathers and analyzes information skillfully and maintains confidentiality.
      • -
      • Interpersonal skills - the individual maintains confidentiality, remains open to others' ideas and exhibits willingness to try new things.
      • -
      • Oral communication - the individual speaks clearly and persuasively in positive or negative situations and demonstrates group presentation skills.
      • -
      • Written communication – the individual edits work for spelling and grammar, presents numerical data effectively and is able to read and interpret written information.
      • -
      • Adaptability - the individual adapts to changes in the work environment, manages competing demands and is able to deal with frequent change, delays or unexpected events.
      • -
      • Dependability - the individual is consistently at work and on time, follows instructions, responds to management direction and solicits feedback to improve performance.
      • -
      • College degree
      • +
      • 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. +
      • 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.
      • +
      + +

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

      +

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

      +
      +
      + + +
      +
      +

      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.

      +

      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. +
      • 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. +
      • 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.
      • +
      • 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.
      • +
      • Excellent interpersonal and communication (written and verbal) skills, the ability to define and solve technical, process and organizational problems, and time management skills.
      • +
      • Proactive, optimistic approach to problem solving.
      • +
      • Commitment to constant personal and organizational improvement.
      • +
      + +

      Preferred qualifications

      +
        +
      • Some teaching experience,
      • +
      • Online course design and development experience.
      • +
      • Experience with Lean and Agile thinking and processes.
      • +
      • Experience with online collaboration tools
      • +
      • Familiarity with video production.
      • +
      • Basic HTML, XML, programming skills.
      • +

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

      -
      +
      -

      DIRECTOR OF PR AND COMMUNICATIONS

      -

      The edX Director of PR & Communications is responsible for creating and executing all PR strategy and providing company-wide leadership to help create and refine the edX core messages and identity as the revolutionary global leader in both on-campus and worldwide education. The Director will design and direct a communications program that conveys cohesive and compelling information about edX's mission, activities, personnel and products while establishing a distinct identity for edX as the leader in online education for both students and learning institutions.

      +

      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:

        -
      • Develop and execute goals and strategy for a comprehensive external and internal communications program focused on driving student engagement around courses and institutional adoption of the edX learning platform.
      • -
      • Work with media, either directly or through our agency of record, to establish edX as the industry leader in global learning.
      • -
      • Work with key influencers including government officials on a global scale to ensure the edX mission, content and tools are embraced and supported worldwide.
      • -
      • Work with marketing colleagues to co-develop and/or monitor and evaluate the content and delivery of all communications messages and collateral.
      • -
      • Initiate and/or plan thought leadership events developed to heighten target-audience awareness; participate in meetings and trade shows
      • -
      • Conduct periodic research to determine communications benchmarks
      • -
      • Inform employees about edX's vision, values, policies, and strategies to enable them to perform their jobs efficiently and drive morale.
      • -
      • Work with and manage existing communications team to effectively meet strategic goals.
      • +
      • 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.
      -

      Qualifications:

      +

      Skills:

        -
      • Ten years of experience in PR and communications
      • -
      • Ability to work creatively and provide company-wide leadership in a fast-paced, dynamic start-up environment required
      • -
      • Adaptability - the individual adapts to changes in the work environment, manages competing demands and is able to deal with frequent change, delays or unexpected events.
      • -
      • Experience in working in successful consumer-focused startups preferred
      • -
      • PR agency experience in setting strategy for complex multichannel, multinational organizations a plus.
      • -
      • Extensive writing experience and simply amazing oral, written, and interpersonal communications skills
      • -
      • B.A./B.S. in communications or related field
      • +
      • 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.

      -
      +
      + + +
      +
      +

      DIRECTOR, PRODUCT MANAGEMENT

      +

      When the power of edX is at its fullest, individuals become the students they had always hoped to be, Professors teach the courses they had always imagined and Universities offer educational opportunities never before seen. None of that happens by accident, so edX is seeking a Product Manager who can keep their eyes on the future and their heart and hands with a team of ferociously intelligent and dedicated technologists. +

      +

      The responsibility of a Product Manager is first and foremost to provide evidence to the development team that what they build will succeed in the marketplace. It is the responsibility of the Product Manager to define the product backlog and the team to build the backlog. The Product Manager is one of the most highly leveraged individuals in the Engineering organization. They work to bring a deep knowledge of the Customer – Students, Professors and Course Staff to the product roadmap. The Product Manager is well-versed in the data and sets the KPI’s that drives the team, the Product Scorecard and the Company Scorecard. They are expected to become experts in the business of online learning, familiar with blended models, MOOC’s and University and Industry needs and the competition. The Product Manager must be able to understand the edX stakeholders. +

      +

      Responsibilities:

      +
        +
      • Assess users’ needs, whether students, Professors or Universities.
      • +
      • Research markets and competitors to provide data driven decisions.
      • +
      • Work with multiple engineering teams, through consensus and with data-backed arguments, in order to provide technology which defines the state of the art for online courses.
      • +
      • Repeatedly build and launch new products and services, complete with the training, documentation and metrics needed to enhance the already impressive brands of the edX partner institutions.
      • +
      • Establish the vision and future direction of the product with input from edX leadership and guidance from partner organizations.
      • +
      • Work in a lean organization, committed to Scrum and Kanban.
      • +
      +

      Qualifications:

      +
        +
      • Bachelor’s degree or higher in a Technical Area
      • +
      • MBA or Masters in Design preferred
      • +
      • Proven ability to develop and implement strategy
      • +
      • Exquisite organizational skills
      • +
      • Deep analytical skills
      • +
      • Social finesse and business sense
      • +
      • Scrum, Kanban
      • +
      • Infatuation with technology, in all its frustrating and fragile complexity
      • +
      • Top flight communication skills, oral and written, with teams which are centrally located and spread all over the world.
      • +
      • Personal commitment and experience of the transformational possibilities of higher education
      • +
      + +

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

      +
      +
      + + +
      +
      +

      CONTENT ENGINEER

      +

      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
      • +
      • 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.
      • +
      • Pushing content to production servers predictably and cleanly.
      • +
      • Sending high volumes of course email adhering to email engine protocols.
      • +
      +

      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
      • +
      • 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
      • +
      • Curiosity to step into the shoes of an online student working to master the course content.
      • +
      • Solid interpersonal skills, especially good listening
      • +
      + +

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

      +
      +
      + + +
      +
      +

      DIRECTOR ENGINEERING, OPEN SOURCE COMMUNITY MANAGER

      +

      In edX courses, students make (and break) electronic circuits, they manipulate molecules on the fly and they do it all at once, in their tens of thousands. We have great Professors and great Universities. But we can’t possibly keep up with all the great ideas out there, so we’re making our platform open source, to turn up the volume on great education. To do that well, we’ll need a Director of Engineering who can lead our Open Source Community efforts.

      +

      Responsibilities:

      +
        +
      • Define and implement software design standards that make the open source community most welcome and productive.
      • +
      • Work with others to establish the governance standards for the edX Open Source Platform, establish the infrastructure, and manage the team to deliver releases and leverage our University partners and stakeholders to
      • make the edX platform the world’s best learning platform. +
      • Help the organization recognize the benefits and limitations inherent in open source solutions.
      • +
      • Establish best practices and key tool usage, especially those based on industry standards.
      • +
      • Provide visibility for the leadership team into the concerns and challenges faced by the open source community.
      • +
      • Foster a thriving community by providing the communication, documentation and feedback that they need to be enthusiastic.
      • +
      • Maximize the good code design coming from the open source community.
      • +
      • Provide the wit and firmness that the community needs to channel their energy productively.
      • +
      • Tactfully balance the internal needs of the organization to pursue new opportunities with the community’s need to participate in the platform’s evolution.
      • +
      • Shorten lines of communication and build trust across entire team
      • +
      +

      Qualifications:

      +
        + +
      • Bachelors, preferably Masters in Computer Science
      • +
      • Solid communication skills, especially written
      • +
      • Committed to Agile practice, Scrum and Kanban
      • +
      • Charm and humor
      • +
      • Deep familiarity with Open Source, participant and contributor
      • +
      • Python, Django, Javascript
      • +
      • Commitment to support your technical recommendations, both within and beyond the organization.
      • +
      + +

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

      +
      +
      + + +
      +
      +

      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.

      + +

      There are a number of projects for which we are recruiting engineers:
      + +

      Learning Management System: We are developing an Open Source Standard that allows for the creation of instructional plug-ins and assessments in our platform. You must have a deep interest in semantics of learning, and able to build services at scale.

      + +

      Forums: We are building our own Forums software because we believe that education requires a forums platform capable of supporting learning communities. We are analytics driven. The ideal Forums candidates are focused on metrics and key performance indicators, understand how to build on top of a service based architecture and are wedded to quick iterations and user feedback. + +

      Analytics: We are looking for a platform engineer who has deep MongoDB or no SQL database experience. Our data infrastructure needs to scale to multiple terabytes. Researchers from Harvard, MIT, Berkeley and edX Universities will use our analytics platform to research and examine the fundamentals of learning. The analytics engineer will be responsible for both building out an analytics platform and a pub-sub and real-time pipeline processing architecture. Together they will allow researchers, students and Professors access to never before seen analytics. + +

      Course Development Authoring Tools: We are committed to making it easy for Professors to develop and publish their courses online. So we are building the tools that allow them to readily convert their vision to an online course ready for thousands of students.

      + +

      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
      • +
      + +

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

      +
      +
      +

      Positions

      How to Apply

      -

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

      +

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

      Our Location

      11 Cambridge Center
      - Cambridge, MA 02142

      + Cambridge, MA 02142

      diff --git a/lms/templates/static_templates/press_releases/bostonx_announcement.html b/lms/templates/static_templates/press_releases/bostonx_announcement.html new file mode 100644 index 0000000000..5aee02dd9e --- /dev/null +++ b/lms/templates/static_templates/press_releases/bostonx_announcement.html @@ -0,0 +1,64 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">City of Boston and edX partner to establish BostonX to improve educational access for residents +
      + + +
      +
      +

      City of Boston and edX partner to establish BostonX to improve educational access for residents

      +
      +
      +

      Pilot project offers online courses, educational support and jobs training through Boston community centers

      + +

      CAMBRIDGE, MA – January 29, 2013 – +EdX, the not-for-profit online learning initiative founded by Harvard University and the Massachusetts Institute of Technology (MIT), announced today a pilot project with the City of Boston, Harvard and MIT to make online courses available through internet-connected Boston neighborhood community centers, high schools and libraries. A first-of-its-kind project, BostonX brings together innovators from the country’s center of higher education to offer Boston residents access to courses, internships, job training and placement services, and locations for edX students to gather, socialize and deepen learning.

      + +

      “We must connect adults and youth in our neighborhoods with the opportunities of the knowledge economy,” said Mayor Tom Menino. “BostonX will help update our neighbors’ skills and our community centers. As a first step, I’m pleased to announce a pilot with Harvard, MIT and edX, their online learning initiative, which will bring free courses and training to our community centers.”

      + +

      BostonX builds on edX’s mission of expanding access to education and delivering high-quality courses on its cutting-edge platform using innovative tools and educational techniques. The City of Boston will provide BostonX sites at community centers with computer access and basic computer training, support for internships, career counseling, and job transitioning. Harvard, MIT and edX will work with the city to provide courses selected to eliminate skills gaps, in-person lessons from affiliated instructors, training in online learning best practices and certificates of mastery for those who successfully complete the courses.

      + +

      “EdX’s innovative content, learning methodologies and game-like laboratories and teaching methods are transforming education, from 16-year-old students in Bangladesh, to community college students at Bunker Hill and MassBay, and now learners across Boston,” said Anant Agarwal, President of edX. “We’re thrilled to be able to partner with Mayor Menino and the City of Boston to provide this first-ever experience and hope that this idea will spread and create a number of CityX’s around the world, including Cambridge, Massachusetts where edX was founded.”

      + +

      This new pilot with the City of Boston follows another edX project with two Boston-area community colleges. This month, Bunker Hill and MassBay Community Colleges began offering an adapted version of the MITx 6.00x Introduction to Computer Science and Programming course at their respective campuses. The BostonX initiative goes one step further by allowing, encouraging and supporting residents of all ages, regardless of social status or neighborhood, to participate in life changing educational opportunities.

      + +

      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.

      + + +
      +

      Contact:

      +

      Brad Baker, Weber Shandwick for edX

      +

      BBaker@webershandwick.com

      +

      (617) 520-7043

      +
      + + +
      +
      +
      diff --git a/lms/templates/static_templates/press_releases/edx_expands_internationally.html b/lms/templates/static_templates/press_releases/edx_expands_internationally.html new file mode 100644 index 0000000000..0ee42dafa9 --- /dev/null +++ b/lms/templates/static_templates/press_releases/edx_expands_internationally.html @@ -0,0 +1,81 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">edX Expands Internationally and Doubles its Institutional Membership with the Addition of Six New Schools +
      + + +
      +
      +

      edX Expands Internationally and Doubles its Institutional Membership with the Addition of Six New Schools

      +
      +
      +

      edX welcomes The Australian National University, Delft University of Technology, École Polytechnique Fédérale de Lausanne, McGill University, Rice University and University of Toronto to its X University Consortium of the world’s leading higher education institutions

      + +

      CAMBRIDGE, MA – Feb. 20, 2013 – +EdX, the not-for-profit online learning enterprise founded by Harvard University and the Massachusetts Institute of Technology (MIT), announced today the international expansion of its X University Consortium with the addition of six new global higher education institutions. The Australian National University (ANU), Delft University of Technology in the Netherlands, École Polytechnique Fédérale de Lausanne (EPFL) in Switzerland, McGill University and the University of Toronto in Canada, and Rice University in the United States are joining the Consortium and will use the edX platform to deliver the next generation of online and blended courses. This international expansion enables edX to better achieve its mission of providing world-class courses to everyone, everywhere, and is the natural next step to continue serving the large international student body already using edX on a daily basis. +

      + +

      While MOOCs, or massive open online courses, have typically focused on offering a variety of online courses inexpensively or for free, edX's vision is much larger. EdX is building an open source educational platform and a network of the world's top universities to improve education both online and on campus while conducting research on how students learn. To date, edX has more than 700,000 individuals on its platform, who account for more than 900,000 course enrollments. The addition of these new higher education institutions stretching from North America to Europe to the Asia Pacific will double the number of X University Consortium members and add a rich variety of new courses to edX’s offerings: +

      + +
        +
      • The Australian National University, a celebrated place of intensive research, education and policy engagement, will provide a series of ANUx courses to the open source platform including Astrophysics taught by Nobel Laureate and Professor of Astrophysics Brian Schmidt and his colleague Dr. Paul Francis, and Engaging India, taught by Dr. McComas Taylor and Dr. Peter Friedlander.
      • + +
      • Delft University of Technology, the largest and oldest technological university in the Netherlands, will provide a series of DelftX courses under Creative Commons license, including Introduction to Aerospace Engineering by Professor Jacco Hoekstra, Solar Energy by Dr. Arno Smets, and Water Treatment Engineering by Professor Jules van Lier.
      • + +
      • École Polytechnique Fédérale de Lausanne, one of the most famous institutions of science and technology in Europe, will provide a series of EPFLx courses specially tailored to fit the edX format, originating from its five schools -- Engineering, Life Sciences, Informatics and Communication, Architecture and Basic Sciences.
      • + +
      • McGill University, one of Canada's best-known institutions of higher learning and one of the leading universities in the world, will provide a series of McGillX courses in areas ranging from science and the humanities to public policy issues.
      • + +
      • Rice University, in Houston, Texas, is consistently ranked among the nation's top 20 universities by U.S. News & World Report. Rice has highly respected schools of Architecture, Business, Continuing Studies, Engineering, Humanities, Music, Natural Sciences and Social Sciences and is home to the Baker Institute for Public Policy. Rice's Smalley Institute for Nanoscale Science and Technology was the world’s first nanotechnology center when it opened in 1991. Rice will initially provide four RiceX courses and investigate ways to integrate its learning analytics tools from OpenStax Tutor to enable students and instructors to track their progress in real time.
      • + +
      • University of Toronto, one of the most respected and influential institutions of higher education and advanced research in the world, will provide a series of TorontoX courses including Terrestrial Energy System by Professor Bryan Kanrey, Behavioral Economics by Professor Dilip Soman, The Logic of Business: Building Blocks for Organizational Design by Professor Mihnea Moldoveanu, and Bioinformatic Methods by Professor Nicholas Provart.
      • +
      + +

      “We have had an international student community from the very beginning, and bringing these leading universities, from North America and Europe and the Asia Pacific into the edX organization will help us meet the tremendous demand we are experiencing,” said Anant Agarwal, President of edX. “Each of these schools was carefully selected for the distinct expertise they bring to our growing family of edX institutions. We remain committed to growing edX to meet the needs of the world while maintaining a superior learning experience for all.”

      + +

      Courses offered by institutions on the edX platform provide the same rigor as on-campus classes but are designed to take advantage of the unique features and benefits of online learning environments, including game-like experiences, instant feedback and cutting-edge virtual laboratories. Through edX, the new X Universities will provide interactive education experiences for students around the world. All that is required of edX students is access to the Internet and a desire to learn. By breaking down the barriers of location and cost and enabling the global exchange of information and ideas, edX is changing the foundations of both teaching and learning.

      + +

      The new member institutions will join founding universities MIT and Harvard, as well as the University of California, Berkeley, the University of Texas System, Wellesley College and Georgetown University in the X University Consortium. ANUx, DelftX, EPFLx, McGillX, RiceX and TorontoX will offer courses on edX beginning in late 2013. All of the courses will be hosted on edX’s open source platform at www.edx.org. +

      + +

      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.

      + + +
      +

      Media Contact:

      +

      Dan O'Connell

      +

      oconnell@edx.org

      +

      (617) 480-6585

      +
      + + +
      +
      +
      diff --git a/lms/templates/static_templates/press_releases/eric_lander_secret_of_life.html b/lms/templates/static_templates/press_releases/eric_lander_secret_of_life.html new file mode 100644 index 0000000000..d91c8091d7 --- /dev/null +++ b/lms/templates/static_templates/press_releases/eric_lander_secret_of_life.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">Human Genome Pioneer Eric Lander to reveal “the secret of life” +
      + + +
      +
      +

      Human Genome Pioneer Eric Lander to reveal “the secret of life”

      +
      +
      +

      Broad Institute Director shares his MIT introductory biology course, covering topics in biochemistry, genetics and genomics, through edX.

      + +
      + +
      +

      Eric Lander, the founding director of the Broad Institute and a professor at MIT and Harvard Medical School.

      + High Resolution Image

      +
      +
      + + + +

      CAMBRIDGE, MA – January 30, 2013 – +In the past 10 years, the ability to decode or “sequence” DNA has grown by a million-fold, a stunning rate of progress that is producing a flood of information about human biology and disease. Because of these advances, the scientific community — and the world as a whole — stands on the verge of a revolution in biology. In the coming decades scientists will be able to understand how cells are “wired” and how that wiring is disrupted in human diseases ranging from diabetes to cancer to schizophrenia. Now, with his free online course, 7.00x Introductory Biology: “The Secret of Life”, genome pioneer Eric Lander, the founding director of the Broad Institute and a professor at MIT and Harvard Medical School, will explain to students around the world the basics of biology – the secret of life, so to speak – so that they can understand today’s revolution in biology.

      + +

      EdX, the not-for-profit online learning initiative founded by Harvard University and the Massachusetts Institute of Technology (MIT), brings the best courses from the best faculty at the best institutions to anyone with an Internet connection. For the past 20 years, legendary teacher Lander has taught Introductory Biology to more than half of all MIT students. He has now adapted his course for online education, creating the newest course on the edX platform. The course, 7.00X, is now open for enrollment, with the first class slated for March 5th. This course will include innovative technology including a 3D molecule viewer and gene explorer tool to transform the learning experience. It is open to all levels and types of learners.

      + +

      “Introducing the freshman class of MIT to the basics of biology is exhilarating,” said Lander. “Now, with this edX course, I look forward to teaching people around the world. There are no prerequisites for this course – other than curiosity and an interest in understanding some of the greatest scientific challenges of our time.”

      + +

      Those taking the course will learn the fundamental ideas that underlie modern biology and medicine, including genetics, biochemistry, molecular biology, recombinant DNA, genomics and genomic medicine. They will become familiar with the structure and function of macromolecules such as DNA, RNA and proteins and understand how information flows within cells. Students will explore how mutations affect biological function and cause human disease. They will learn about modern molecular biological techniques and their wide-ranging impact.

      + +

      “Eric Lander has created this remarkable digitally enhanced introduction to genetics and biology,” said Anant Agarwal, President of edX. “With this unique online version, he has brought the introductory biology course to a new level. It has been completely rethought and retooled, incorporating cutting-edge online interactive tools as well as community-building contests and milestone-based prizes.”

      + +

      With online courses through edX like 7.00x, what matters isn’t what people have achieved or their transcripts, but their desire to learn. Students only need to come with a real interest in science and the desire to understand what's going on at the forefront of biology, and to learn the fundamental principles on which an amazing biomedical revolution is based – from one of the top scientist in the world. 7.00x Introductory Biology: The Secret of Life is now available for enrollment. Classes will start on March 5, 2013.

      + +

      Dr. Eric Lander is President and Founding Director of the Broad Institute of Harvard and MIT, a new kind of collaborative biomedical research institution focused on genomic medicine. Dr. Lander is also Professor of Biology at MIT and Professor of Systems Biology at the Harvard Medical School. In addition, Dr. Lander serves as Co-Chair of the President’s Council of Advisors on Science and Technology, which advises the White House on science and technology. A geneticist, molecular biologist and mathematician, Dr. Lander has played a pioneering role in all aspects of the reading, understanding and medical application of the human genome. He was a principal leader of the international Human Genome Project (HGP) from 1990-2003, with his group being the largest contributor to the mapping and sequencing of the human genetic blueprint. Dr. Lander was an early pioneer in the free availability of genomic tools and information. Finally, he has mentored an extraordinary cadre of young scientists who have become the next generation of leaders in medical genomics. The recipient of numerous awards and honorary degrees, Dr. Lander was elected a member of the U.S. National Academy of Sciences in 1997 and of the U.S. Institute of Medicine in 1999.

      + + +

      Previously announced new 2013 courses include: +8.02x Electricity and Magnetism from Walter Lewin +Justice from Michael Sandel; Introduction to Statistics from Ani Adhikari; The Challenges of Global Poverty from Esther Duflo; The Ancient Greek Hero from Gregory Nagy; Quantum Mechanics and Quantum Computation from Umesh Vazirani; Human Health and Global Environmental Change, from Aaron Bernstein and Jack Spengler.

      + +

      In addition to these new courses, edX is bringing back several courses from the popular fall 2012 semester: Introduction to Computer Science and Programming; Introduction to Solid State Chemistry; Introduction to Artificial Intelligence; Software as a Service I; Software as a Service II; Foundations of Computer Graphics.

      + +

      About the Broad Institute of MIT and Harvard

      + +

      The Eli and Edythe L. Broad Institute of MIT and Harvard was founded in 2003 to empower this generation of creative scientists to transform medicine with new genome-based knowledge. The Broad Institute seeks to describe all the molecular components of life and their connections; discover the molecular basis of major human diseases; develop effective new approaches to diagnostics and therapeutics; and disseminate discoveries, tools, methods and data openly to the entire scientific community.

      + +

      Founded by MIT, Harvard and its affiliated hospitals, and the visionary Los Angeles philanthropists Eli and Edythe L. Broad, the Broad Institute includes faculty, professional staff and students from throughout the MIT and Harvard biomedical research communities and beyond, with collaborations spanning over a hundred private and public institutions in more than 40 countries worldwide. For further information about the Broad Institute, go to www.broadinstitute.org.

      + +

      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.

      + +
      +

      Contact:

      +

      Brad Baker, Weber Shandwick for edX

      +

      BBaker@webershandwick.com

      +

      (617) 520-7043

      +
      + + + +
      +
      +
      diff --git a/lms/templates/static_templates/press_releases/template.html b/lms/templates/static_templates/press_releases/template.html new file mode 100644 index 0000000000..52eebf49f5 --- /dev/null +++ b/lms/templates/static_templates/press_releases/template.html @@ -0,0 +1,60 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">TITLE +
      + + +
      +
      +

      TITLE

      +
      +
      +

      SUBTITLE

      + +

      CAMBRIDGE, MA – MONTH DAY, YEAR – + +Text

      + +

      more text

      + + +

      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.

      + + +
      +

      Media Contact:

      +

      Dan O'Connell

      +

      oconnell@edx.org

      +

      (617) 480-6585

      +
      + + +
      +
      +
      diff --git a/lms/templates/test_center_register.html b/lms/templates/test_center_register.html index f6c53c0e89..6b87860fad 100644 --- a/lms/templates/test_center_register.html +++ b/lms/templates/test_center_register.html @@ -466,7 +466,7 @@ Last Eligible Appointment Date: ${exam_info.last_eligible_appointment_date_text}
    6. - Registration End Date: ${exam_info.registration_end_date_text} + Registration Ends: ${exam_info.registration_end_date_text}
    7. % endif diff --git a/lms/templates/university_profile/anux.html b/lms/templates/university_profile/anux.html new file mode 100644 index 0000000000..c19310c70f --- /dev/null +++ b/lms/templates/university_profile/anux.html @@ -0,0 +1,24 @@ +<%inherit file="base.html" /> +<%namespace name='static' file='../static_content.html'/> + +<%block name="title">ANUx + +<%block name="university_header"> + + + + +<%block name="university_description"> +

      The Australian National University (ANU) is a celebrated place of intensive research, education and policy engagement. Our research has always been central to everything we do, shaping a holistic learning experience that goes beyond the classroom, giving students access to researchers who are among the best in their fields and to opportunities for development around Australia and the world.

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

      Delft University of Technology is the largest and oldest technological university in the Netherlands. Our research is inspired by the desire to increase fundamental understanding, as well as by societal challenges. We encourage our students to be independent thinkers so they will become engineers capable of solving complex problems. Our students have chosen Delft University of Technology because of our reputation for quality education and research.

      + + +${parent.body()} diff --git a/lms/templates/university_profile/epflx.html b/lms/templates/university_profile/epflx.html new file mode 100644 index 0000000000..5119a223de --- /dev/null +++ b/lms/templates/university_profile/epflx.html @@ -0,0 +1,27 @@ +<%inherit file="base.html" /> +<%namespace name='static' file='../static_content.html'/> + +<%block name="title">EPFLx + +<%block name="university_header"> + + + + +<%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. +

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

      McGill University is one of Canada's best-known institutions of higher learning and one of the leading universities in the world. McGill is located in vibrant multicultural Montreal, in the province of Quebec. Our 11 faculties and 11 professional schools offer more than 300 programs to some 38,000 graduate, undergraduate and continuing studies students. McGill ranks 1st in Canada among medical-doctoral universities (Maclean’s) and 18th in the world (QS World University Rankings).

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

      Located on a 300-acre forested campus in Houston, Rice University is consistently ranked among the nation's top 20 universities by U.S. News & World Report. Rice has highly respected schools of Architecture, Business, Continuing Studies, Engineering, Humanities, Music, Natural Sciences and Social Sciences and is home to the Baker Institute for Public Policy. With 3,708 undergraduates and 2,374 graduate students, Rice's undergraduate student-to-faculty ratio is 6-to-1. Its residential college system builds close-knit communities and lifelong friendships, just one reason why Rice has been ranked No. 1 for best quality of life multiple times by the Princeton Review and No. 2 for "best value" among private universities by Kiplinger's Personal Finance.

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

      + + +${parent.body()} diff --git a/lms/templates/university_profile/torontox.html b/lms/templates/university_profile/torontox.html new file mode 100644 index 0000000000..6ae50d21b9 --- /dev/null +++ b/lms/templates/university_profile/torontox.html @@ -0,0 +1,24 @@ +<%inherit file="base.html" /> +<%namespace name='static' file='../static_content.html'/> + +<%block name="title">University of TorontoX + +<%block name="university_header"> + + + + +<%block name="university_description"> +

      Established in 1827, the University of Toronto is a vibrant and diverse academic community. It includes 80,000 students, 12,000 colleagues holding faculty appointments, 200 librarians, and 6,000 staff members across three distinctive campuses and at many partner sites, including world-renowned hospitals. With over 800 undergraduate programs, 150 graduate programs, and 40 professional programs, U of T attracts students of the highest calibre, from across Canada and from 160 countries around the world. The University is one of the most respected and influential institutions of higher education and advanced research in the world. Its strengths extend across the full range of disciplines: the 2012-13 Times Higher Education ranking groups the University of Toronto with Stanford, UC Berkeley, UCLA, Columbia, Cambridge, Oxford, the University of Melbourne, and the University of Michigan as the only institutions in the top 27 in all 6 broad disciplinary areas. The University is also consistently rated one of Canada’s Top 100 employers, and ranks with Harvard and Yale for the top university library resources in North America.

      + + +${parent.body()} diff --git a/lms/templates/video.html b/lms/templates/video.html index 6cee9ed39b..afbb9015ee 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -2,9 +2,19 @@

      ${display_name}

      % endif - %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
      + +%elif settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id: + + + + + %else:
      diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html new file mode 100644 index 0000000000..2028d3c320 --- /dev/null +++ b/lms/templates/videoalpha.html @@ -0,0 +1,43 @@ +% if display_name is not UNDEFINED and display_name is not None: +

      ${display_name}

      +% endif + +%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: +
      +%else: +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +%endif + +% if sources.get('main'): +
      +

      Download video here.

      +
      +% endif + +% if track: +
      +

      Download subtitles here.

      +
      +% endif diff --git a/lms/urls.py b/lms/urls.py index 9d590387e4..5e5ac9a7f2 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -2,6 +2,7 @@ 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 import django.contrib.auth.views # Uncomment the next two lines to enable the admin: @@ -13,7 +14,7 @@ urlpatterns = ('', # certificate view url(r'^update_certificate$', 'certificates.views.update_certificate'), - url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware + url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware url(r'^dashboard$', 'student.views.dashboard', name="dashboard"), url(r'^admin_dashboard$', 'dashboard.views.dashboard'), @@ -35,7 +36,7 @@ urlpatterns = ('', # url(r'^testcenter/logout$', 'student.test_center_views.logout'), url(r'^event$', 'track.views.user_track'), - url(r'^t/(?P