diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 51a98f2de7..b7d1d7a5ce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,9 @@ Studio: Send e-mails to new Studio users (on edge only) when their course creato status has changed. This will not be in use until the course creator table is enabled. +Studio: Added improvements to Course Creation: richer error messaging, tip +text, and fourth field for course run. + LMS: Added user preferences (arbitrary user/key/value tuples, for which which user/key is unique) and a REST API for reading users and preferences. Access to the REST API is restricted by use of the @@ -18,6 +21,9 @@ the setting is not present, the API is disabled). LMS: Added endpoints for AJAX requests to enable/disable notifications (which are not yet implemented) and a one-click unsubscribe page. +Studio: Allow instructors of a course to designate other staff as instructors; +this allows instructors to hand off management of a course to someone else. + Common: Add a manage.py that knows about edx-platform specific settings and projects Common: Added *experimental* support for jsinput type. diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py index 0f2e60dd6e..4923851445 100644 --- a/cms/djangoapps/auth/authz.py +++ b/cms/djangoapps/auth/authz.py @@ -178,10 +178,12 @@ def _remove_user_from_group(user, group_name): user.save() -def is_user_in_course_group_role(user, location, role): +def is_user_in_course_group_role(user, location, role, check_staff=True): 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 + if check_staff and user.is_staff: + return True + return user.groups.filter(name=get_course_groupname_for_role(location, role)).count() > 0 return False diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 7e1e6470ff..7ea09333ed 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -1,7 +1,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore -from lxml import html +from lxml import html, etree import re from django.http import HttpResponseBadRequest import logging @@ -74,34 +74,44 @@ def update_course_updates(location, update, passed_id=None): escaped = django.utils.html.escape(course_updates.data) course_html_parsed = html.fromstring("
  1. " + escaped + "
") + # if there's no ol, create it + if course_html_parsed.tag != 'ol': + # surround whatever's there w/ an ol + if course_html_parsed.tag != 'li': + # but first wrap in an li + li = etree.Element('li') + li.append(course_html_parsed) + course_html_parsed = li + ol = etree.Element('ol') + ol.append(course_html_parsed) + course_html_parsed = ol + # No try/catch b/c failure generates an error back to client new_html_parsed = html.fromstring('
  • ' + update['date'] + '

    ' + update['content'] + '
  • ') - # 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? - if passed_id is not None: - idx = get_idx(passed_id) - # idx is count from end of list - course_html_parsed[-idx] = new_html_parsed - else: - course_html_parsed.insert(0, new_html_parsed) + # ??? Should this use the id in the json or in the url or does it matter? + if passed_id is not None: + idx = get_idx(passed_id) + # idx is count from end of list + course_html_parsed[-idx] = new_html_parsed + else: + course_html_parsed.insert(0, new_html_parsed) - idx = len(course_html_parsed) - passed_id = course_updates.location.url() + "/" + str(idx) + idx = len(course_html_parsed) + passed_id = course_updates.location.url() + "/" + str(idx) - # update db record - course_updates.data = html.tostring(course_html_parsed) - modulestore('direct').update_item(location, course_updates.data) + # update db record + course_updates.data = html.tostring(course_html_parsed) + modulestore('direct').update_item(location, course_updates.data) - if (len(new_html_parsed) == 1): - content = new_html_parsed[0].tail - else: - content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]]) + if (len(new_html_parsed) == 1): + content = new_html_parsed[0].tail + else: + content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]]) - return {"id": passed_id, - "date": update['date'], - "content": content} + return {"id": passed_id, + "date": update['date'], + "content": content} def delete_course_update(location, update, passed_id): diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index d357c8ae96..43b2b64257 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -53,6 +53,14 @@ def i_have_opened_a_new_course(_step): open_new_course() +@step('(I select|s?he selects) the new course') +def select_new_course(_step, whom): + course_link_xpath = '//div[contains(@class, "courses")]//a[contains(@class, "class-link")]//span[contains(., "{name}")]/..'.format( + name="Robot Super Course") + element = world.browser.find_by_xpath(course_link_xpath) + element.click() + + @step(u'I press the "([^"]*)" notification button$') def press_the_notification_button(_step, name): css = 'a.action-%s' % name.lower() @@ -118,14 +126,18 @@ def create_studio_user( registration.register(studio_user) registration.activate() + return studio_user + def fill_in_course_info( name='Robot Super Course', org='MITx', - num='999'): + num='101', + run='2013_Spring'): world.css_fill('.new-course-name', name) world.css_fill('.new-course-org', org) world.css_fill('.new-course-number', num) + world.css_fill('.new-course-run', run) def log_into_studio( @@ -242,7 +254,7 @@ def save_button_disabled(step): @step('I confirm the prompt') def confirm_the_prompt(step): prompt_css = 'a.button.action-primary' - world.css_click(prompt_css) + world.css_click(prompt_css, success_condition=lambda: not world.css_visible(prompt_css)) @step(u'I am shown a (.*)$') @@ -252,6 +264,7 @@ def i_am_shown_a_notification(step, notification_type): def type_in_codemirror(index, text): world.css_click(".CodeMirror", index=index) + world.browser.execute_script("$('div.CodeMirror.CodeMirror-focused > div').css('overflow', '')") g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea") if world.is_mac(): g._element.send_keys(Keys.COMMAND + 'a') diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 2b206e4466..513eb699e9 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -12,11 +12,20 @@ def create_component_instance(step, component_button_css, category, has_multiple_templates=True): click_new_component_button(step, component_button_css) + if category in ('problem', 'html'): + def animation_done(_driver): + return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none' + world.wait_for(animation_done) if has_multiple_templates: click_component_from_menu(category, boilerplate, expected_css) - assert_equal(1, len(world.css_find(expected_css))) + assert_equal( + 1, + len(world.css_find(expected_css)), + "Component instance with css {css} was not created successfully".format(css=expected_css)) + + @world.absorb def click_new_component_button(step, component_button_css): @@ -39,11 +48,13 @@ def click_component_from_menu(category, boilerplate, expected_css): elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category) elements = world.css_find(elem_css) assert_equal(len(elements), 1) - world.css_click(elem_css) + world.wait_for(lambda _driver: world.css_visible(elem_css)) + world.css_click(elem_css, success_condition=lambda: 1 == len(world.css_find(expected_css))) @world.absorb def edit_component_and_select_settings(): + world.wait_for(lambda _driver: world.css_visible('a.edit-button')) world.css_click('a.edit-button') world.css_click('#settings-mode') diff --git a/cms/djangoapps/contentstore/features/course-overview.feature b/cms/djangoapps/contentstore/features/course-overview.feature index b3041b9b18..a9aed5d982 100644 --- a/cms/djangoapps/contentstore/features/course-overview.feature +++ b/cms/djangoapps/contentstore/features/course-overview.feature @@ -63,3 +63,10 @@ Feature: Course Overview When I navigate to the course overview page And I change an assignment's grading status Then I am shown a notification + + Scenario: Notification is shown on subsection reorder + Given I have opened a new course section in Studio + And I have added a new subsection + And I have added a new subsection + When I reorder subsections + Then I am shown a notification diff --git a/cms/djangoapps/contentstore/features/course-overview.py b/cms/djangoapps/contentstore/features/course-overview.py index 10fa6453b2..8baad68fdf 100644 --- a/cms/djangoapps/contentstore/features/course-overview.py +++ b/cms/djangoapps/contentstore/features/course-overview.py @@ -124,3 +124,14 @@ def all_sections_are_collapsed(step): def change_grading_status(step): world.css_find('a.menu-toggle').click() world.css_find('.menu li').first.click() + + +@step(u'I reorder subsections') +def reorder_subsections(_step): + draggable_css = 'a.drag-handle' + ele = world.css_find(draggable_css).first + ele.action_chains.drag_and_drop_by_offset( + ele._element, + 30, + 0 + ).perform() diff --git a/cms/djangoapps/contentstore/features/course-team.feature b/cms/djangoapps/contentstore/features/course-team.feature index fc1212f398..ecce174ca2 100644 --- a/cms/djangoapps/contentstore/features/course-team.feature +++ b/cms/djangoapps/contentstore/features/course-team.feature @@ -1,7 +1,7 @@ Feature: Course Team As a course author, I want to be able to add others to my team - Scenario: Users can add other users + Scenario: Admins can add other users Given I have opened a new course in Studio And the user "alice" exists And I am viewing the course team settings @@ -9,7 +9,7 @@ Feature: Course Team And "alice" logs in Then she does see the course on her page - Scenario: Added users cannot delete or add other users + Scenario: Added admins cannot delete or add other users Given I have opened a new course in Studio And the user "bob" exists And I am viewing the course team settings @@ -18,7 +18,7 @@ Feature: Course Team Then he cannot delete users And he cannot add users - Scenario: Users can delete other users + Scenario: Admins can delete other users Given I have opened a new course in Studio And the user "carol" exists And I am viewing the course team settings @@ -27,8 +27,33 @@ Feature: Course Team And "carol" logs in Then she does not see the course on her page - Scenario: Users cannot add users that do not exist + Scenario: Admins cannot add users that do not exist Given I have opened a new course in Studio And I am viewing the course team settings When I add "dennis" to the course team Then I should see "Could not find user by email address" somewhere on the page + + Scenario: Admins should be able to make other people into admins + Given I have opened a new course in Studio + And the user "emily" exists + And I am viewing the course team settings + And I add "emily" to the course team + When I make "emily" a course team admin + And "emily" logs in + And she selects the new course + And she views the course team settings + Then "emily" should be marked as an admin + And she can add users + And she can delete users + + Scenario: Admins should be able to remove other admins + Given I have opened a new course in Studio + And the user "frank" exists as a course admin + And I am viewing the course team settings + When I remove admin rights from "frank" + And "frank" logs in + And he selects the new course + And he views the course team settings + Then "frank" should not be marked as an admin + And he cannot add users + And he cannot delete users diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py index ad5d31977c..18456b15f7 100644 --- a/cms/djangoapps/contentstore/features/course-team.py +++ b/cms/djangoapps/contentstore/features/course-team.py @@ -3,65 +3,105 @@ from lettuce import world, step from common import create_studio_user, log_into_studio +from django.contrib.auth.models import Group +from auth.authz import get_course_groupname_for_role PASSWORD = 'test' EMAIL_EXTENSION = '@edx.org' -@step(u'I am viewing the course team settings') -def view_grading_settings(_step): +@step(u'(I am viewing|s?he views) the course team settings') +def view_grading_settings(_step, whom): world.click_course_settings() link_css = 'li.nav-course-settings-team a' world.css_click(link_css) -@step(u'the user "([^"]*)" exists$') -def create_other_user(_step, name): - create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION)) +@step(u'the user "([^"]*)" exists( as a course admin)?$') +def create_other_user(_step, name, course_admin): + user = create_studio_user(uname=name, password=PASSWORD, email=(name + EMAIL_EXTENSION)) + if course_admin: + location = world.scenario_dict["COURSE"].location + for role in ("staff", "instructor"): + group, __ = Group.objects.get_or_create(name=get_course_groupname_for_role(location, role)) + user.groups.add(group) + user.save() @step(u'I add "([^"]*)" to the course team') def add_other_user(_step, name): - new_user_css = 'a.new-user-button' + new_user_css = 'a.create-user-button' world.css_click(new_user_css) + world.wait(0.5) - email_css = 'input.email-input' + email_css = 'input#user-email-input' f = world.css_find(email_css) f._element.send_keys(name, EMAIL_EXTENSION) - confirm_css = '#add_user' + confirm_css = 'form.create-user button.action-primary' world.css_click(confirm_css) @step(u'I delete "([^"]*)" from the course team') def delete_other_user(_step, name): - to_delete_css = 'a.remove-user[data-id="{name}{extension}"]'.format(name=name, extension=EMAIL_EXTENSION) + to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format( + email="{0}{1}".format(name, EMAIL_EXTENSION)) world.css_click(to_delete_css) +@step(u'I make "([^"]*)" a course team admin') +def make_course_team_admin(_step, name): + admin_btn_css = '.user-item[data-email="{email}"] .user-actions .add-admin-role'.format( + email=name+EMAIL_EXTENSION) + world.css_click(admin_btn_css) + + +@step(u'I remove admin rights from "([^"]*)"') +def remove_course_team_admin(_step, name): + admin_btn_css = '.user-item[data-email="{email}"] .user-actions .remove-admin-role'.format( + email=name+EMAIL_EXTENSION) + world.css_click(admin_btn_css) + + @step(u'"([^"]*)" logs in$') def other_user_login(_step, name): log_into_studio(uname=name, password=PASSWORD, email=name + EMAIL_EXTENSION) @step(u's?he does( not)? see the course on (his|her) page') -def see_course(_step, doesnt_see_course, gender): +def see_course(_step, inverted, gender): class_css = 'span.class-name' all_courses = world.css_find(class_css, wait_time=1) all_names = [item.html for item in all_courses] - if doesnt_see_course: + if inverted: assert not world.scenario_dict['COURSE'].display_name in all_names else: assert world.scenario_dict['COURSE'].display_name in all_names -@step(u's?he cannot delete users') -def cannot_delete(_step): +@step(u'"([^"]*)" should( not)? be marked as an admin') +def marked_as_admin(_step, name, inverted): + flag_css = '.user-item[data-email="{email}"] .flag-role.flag-role-admin'.format( + email=name+EMAIL_EXTENSION) + if inverted: + assert world.is_css_not_present(flag_css) + else: + assert world.is_css_present(flag_css) + + +@step(u's?he can(not)? delete users') +def can_delete_users(_step, inverted): to_delete_css = 'a.remove-user' - assert world.is_css_not_present(to_delete_css) + if inverted: + assert world.is_css_not_present(to_delete_css) + else: + assert world.is_css_present(to_delete_css) -@step(u's?he cannot add users') -def cannot_add(_step): - add_css = 'a.new-user' - assert world.is_css_not_present(add_css) +@step(u's?he can(not)? add users') +def can_add_users(_step, inverted): + add_css = 'a.create-user-button' + if inverted: + assert world.is_css_not_present(add_css) + else: + assert world.is_css_present(add_css) diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index 565a35f802..d7ccb557ba 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -155,6 +155,10 @@ def cancel_does_not_save_changes(step): @step('I have created a LaTeX Problem') def create_latex_problem(step): world.click_new_component_button(step, '.large-problem-icon') + + def animation_done(_driver): + return world.browser.evaluate_script("$('div.new-component').css('display')") == 'none' + world.wait_for(animation_done) # Go to advanced tab. world.css_click('#ui-id-2') world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule') diff --git a/cms/djangoapps/contentstore/features/signup.feature b/cms/djangoapps/contentstore/features/signup.feature index 01c912deca..c249ad61e8 100644 --- a/cms/djangoapps/contentstore/features/signup.feature +++ b/cms/djangoapps/contentstore/features/signup.feature @@ -8,8 +8,7 @@ Feature: Sign in 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 - Then I should see be on the studio home page - And I should see the message "complete your sign up we need you to verify your email address" + Then I should see an email verification prompt Scenario: Login with a valid redirect Given I have opened a new course in Studio diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index e9abb55a78..94c6e6f18e 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -22,14 +22,10 @@ def i_press_the_button_on_the_registration_form(step): world.css_click(submit_css) -@step('I should see be on the studio home page$') -def i_should_see_be_on_the_studio_home_page(step): - step.given('I should see the message "My Courses"') - - -@step(u'I should see the message "([^"]*)"$') -def i_should_see_the_message(step, msg): - assert world.browser.is_text_present(msg, 5) +@step('I should see an email verification prompt') +def i_should_see_an_email_verification_prompt(step): + world.css_has_text('h1.page-header', u'My Courses') + world.css_has_text('div.msg h3.title', u'We need to verify your email address') @step(u'I fill in and submit the signin form$') diff --git a/cms/djangoapps/contentstore/features/upload.py b/cms/djangoapps/contentstore/features/upload.py index 0c700956e3..df63b26b3b 100644 --- a/cms/djangoapps/contentstore/features/upload.py +++ b/cms/djangoapps/contentstore/features/upload.py @@ -58,7 +58,7 @@ def delete_file(_step, file_name): world.css_click(delete_css, index=index) prompt_confirm_css = 'li.nav-item > a.action-primary' - world.css_click(prompt_confirm_css) + world.css_click(prompt_confirm_css, success_condition=lambda: not world.css_visible(prompt_confirm_css)) @step(u'I should see only one "([^"]*)"$') diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py index 93d638e621..6113f42c91 100644 --- a/cms/djangoapps/contentstore/features/video-editor.py +++ b/cms/djangoapps/contentstore/features/video-editor.py @@ -19,5 +19,6 @@ def i_see_the_correct_settings_and_values(step): @step('I have set "show captions" to (.*)') def set_show_captions(step, setting): world.css_click('a.edit-button') + world.wait_for(lambda _driver: world.css_visible('a.save-button')) world.browser.select('Show Captions', setting) world.css_click('a.save-button') diff --git a/cms/djangoapps/contentstore/management/commands/clone.py b/cms/djangoapps/contentstore/management/commands/clone.py index 0ca50acb50..f20625d7f2 100644 --- a/cms/djangoapps/contentstore/management/commands/clone.py +++ b/cms/djangoapps/contentstore/management/commands/clone.py @@ -1,6 +1,6 @@ -### -### Script for cloning a course -### +""" +Script for cloning a course +""" from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.store_utilities import clone_course from xmodule.modulestore.django import modulestore @@ -15,23 +15,25 @@ from auth.authz import _copy_course_group class Command(BaseCommand): + """Clone a MongoDB-backed course to another location""" help = 'Clone a MongoDB backed course to another location' def handle(self, *args, **options): + "Execute the command" if len(args) != 2: raise CommandError("clone requires two arguments: ") source_location_str = args[0] dest_location_str = args[1] - ms = modulestore('direct') - cs = contentstore() + mstore = modulestore('direct') + cstore = contentstore() - print "Cloning course {0} to {1}".format(source_location_str, dest_location_str) + print("Cloning course {0} to {1}".format(source_location_str, dest_location_str)) source_location = CourseDescriptor.id_to_location(source_location_str) dest_location = CourseDescriptor.id_to_location(dest_location_str) - if clone_course(ms, cs, source_location, dest_location): - print "copying User permissions..." + if clone_course(mstore, cstore, source_location, dest_location): + print("copying User permissions...") _copy_course_group(source_location, dest_location) diff --git a/cms/djangoapps/contentstore/management/commands/dump_course_structure.py b/cms/djangoapps/contentstore/management/commands/dump_course_structure.py index d9b7c55cbd..139c603172 100644 --- a/cms/djangoapps/contentstore/management/commands/dump_course_structure.py +++ b/cms/djangoapps/contentstore/management/commands/dump_course_structure.py @@ -1,3 +1,6 @@ +""" +Script for dumping course dumping the course structure +""" from django.core.management.base import BaseCommand, CommandError from xmodule.course_module import CourseDescriptor from xmodule.modulestore.django import modulestore @@ -9,10 +12,14 @@ filter_list = ['xml_attributes', 'checklists'] class Command(BaseCommand): + """ + The Django command for dumping course structure + """ help = '''Write out to stdout a structural and metadata information about a course in a flat dictionary serialized in a JSON format. This can be used for analytics.''' def handle(self, *args, **options): + "Execute the command" if len(args) < 2 or len(args) > 3: raise CommandError("dump_course_structure requires two or more arguments: ||") @@ -32,7 +39,7 @@ class Command(BaseCommand): try: course = store.get_item(loc, depth=4) except: - print 'Could not find course at {0}'.format(course_id) + print('Could not find course at {0}'.format(course_id)) return info = {} diff --git a/cms/djangoapps/contentstore/management/commands/export.py b/cms/djangoapps/contentstore/management/commands/export.py index 90db8750d9..efeb5dc339 100644 --- a/cms/djangoapps/contentstore/management/commands/export.py +++ b/cms/djangoapps/contentstore/management/commands/export.py @@ -1,6 +1,6 @@ -### -### Script for exporting courseware from Mongo to a tar.gz file -### +""" +Script for exporting courseware from Mongo to a tar.gz file +""" import os from django.core.management.base import BaseCommand, CommandError @@ -10,20 +10,21 @@ from xmodule.contentstore.django import contentstore from xmodule.course_module import CourseDescriptor -unnamed_modules = 0 - - class Command(BaseCommand): + """ + Export the specified data directory into the default ModuleStore + """ help = 'Export the specified data directory into the default ModuleStore' def handle(self, *args, **options): + "Execute the command" if len(args) != 2: raise CommandError("export requires two arguments: ") course_id = args[0] output_path = args[1] - print "Exporting course id = {0} to {1}".format(course_id, output_path) + print("Exporting course id = {0} to {1}".format(course_id, output_path)) location = CourseDescriptor.id_to_location(course_id) diff --git a/cms/djangoapps/contentstore/management/commands/export_all_courses.py b/cms/djangoapps/contentstore/management/commands/export_all_courses.py index 69cfb298fb..2118551138 100644 --- a/cms/djangoapps/contentstore/management/commands/export_all_courses.py +++ b/cms/djangoapps/contentstore/management/commands/export_all_courses.py @@ -1,8 +1,6 @@ -### -### Script for exporting all courseware from Mongo to a directory -### -import os - +""" +Script for exporting all courseware from Mongo to a directory +""" from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.django import modulestore @@ -10,13 +8,12 @@ from xmodule.contentstore.django import contentstore from xmodule.course_module import CourseDescriptor -unnamed_modules = 0 - - class Command(BaseCommand): + """Export all courses from mongo to the specified data directory""" help = 'Export all courses from mongo to the specified data directory' def handle(self, *args, **options): + "Execute the command" if len(args) != 1: raise CommandError("export requires one argument: ") @@ -27,14 +24,14 @@ class Command(BaseCommand): root_dir = output_path courses = ms.get_courses() - print "%d courses to export:" % len(courses) + print("%d courses to export:" % len(courses)) cids = [x.id for x in courses] - print cids + print(cids) for course_id in cids: - print "-"*77 - print "Exporting course id = {0} to {1}".format(course_id, output_path) + print("-"*77) + print("Exporting course id = {0} to {1}".format(course_id, output_path)) if 1: try: @@ -42,6 +39,6 @@ class Command(BaseCommand): course_dir = course_id.replace('/', '...') export_to_xml(ms, cs, location, root_dir, course_dir, modulestore()) except Exception as err: - print "="*30 + "> Oops, failed to export %s" % course_id - print "Error:" - print err + print("="*30 + "> Oops, failed to export %s" % course_id) + print("Error:") + print(err) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 9b919daad0..46f439b055 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -1,6 +1,6 @@ -### -### Script for importing courseware from XML format -### +""" +Script for importing courseware from XML format +""" from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.xml_importer import import_from_xml @@ -8,13 +8,14 @@ from xmodule.modulestore.django import modulestore from xmodule.contentstore.django import contentstore -unnamed_modules = 0 - - class Command(BaseCommand): + """ + Import the specified data directory into the default ModuleStore + """ help = 'Import the specified data directory into the default ModuleStore' def handle(self, *args, **options): + "Execute the command" if len(args) == 0: raise CommandError("import requires at least one argument: [...]") @@ -23,8 +24,8 @@ class Command(BaseCommand): course_dirs = args[1:] else: course_dirs = None - print "Importing. Data_dir={data}, course_dirs={courses}".format( + print("Importing. Data_dir={data}, course_dirs={courses}".format( data=data_dir, - courses=course_dirs) + courses=course_dirs)) import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False, static_content_store=contentstore(), verbose=True) diff --git a/cms/djangoapps/contentstore/management/commands/xlint.py b/cms/djangoapps/contentstore/management/commands/xlint.py index 21c8e7d1f8..835b8b84df 100644 --- a/cms/djangoapps/contentstore/management/commands/xlint.py +++ b/cms/djangoapps/contentstore/management/commands/xlint.py @@ -1,18 +1,17 @@ +""" +Verify the structure of courseware as to it's suitability for import +To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)] +""" from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.xml_importer import perform_xlint -unnamed_modules = 0 - - class Command(BaseCommand): - help = \ - ''' - Verify the structure of courseware as to it's suitability for import - To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)] - ''' + """Verify the structure of courseware as to it's suitability for import""" + help = "Verify the structure of courseware as to it's suitability for import" def handle(self, *args, **options): + "Execute the command" if len(args) == 0: raise CommandError("import requires at least one argument: [...]") @@ -21,7 +20,7 @@ class Command(BaseCommand): course_dirs = args[1:] else: course_dirs = None - print "Importing. Data_dir={data}, course_dirs={courses}".format( + print("Importing. Data_dir={data}, course_dirs={courses}".format( data=data_dir, - courses=course_dirs) + courses=course_dirs)) perform_xlint(data_dir, course_dirs, load_error_modules=False) diff --git a/cms/djangoapps/contentstore/tests/test_assets.py b/cms/djangoapps/contentstore/tests/test_assets.py index 58aee3c77d..cde40d502e 100644 --- a/cms/djangoapps/contentstore/tests/test_assets.py +++ b/cms/djangoapps/contentstore/tests/test_assets.py @@ -50,9 +50,9 @@ class UploadTestCase(CourseTestCase): @skip("CorruptGridFile error on continuous integration server") def test_happy_path(self): - file = BytesIO("sample content") - file.name = "sample.txt" - resp = self.client.post(self.url, {"name": "my-name", "file": file}) + f = BytesIO("sample content") + f.name = "sample.txt" + resp = self.client.post(self.url, {"name": "my-name", "file": f}) self.assert2XX(resp.status_code) def test_no_file(self): diff --git a/cms/djangoapps/contentstore/tests/test_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py index 02999f6567..5a99c37fbb 100644 --- a/cms/djangoapps/contentstore/tests/test_checklists.py +++ b/cms/djangoapps/contentstore/tests/test_checklists.py @@ -1,5 +1,5 @@ """ Unit tests for checklist methods in views.py. """ -from contentstore.utils import get_modulestore, get_url_reverse +from contentstore.utils import get_modulestore from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.tests.factories import CourseFactory from django.core.urlresolvers import reverse @@ -27,6 +27,7 @@ class ChecklistTestCase(CourseTestCase): """ self.assertEqual(persisted['short_description'], request['short_description']) compare_urls = (persisted.get('action_urls_expanded') == request.get('action_urls_expanded')) + pers, req = None, None for pers, req in zip(persisted['items'], request['items']): self.assertEqual(pers['short_description'], req['short_description']) self.assertEqual(pers['long_description'], req['long_description']) @@ -38,7 +39,11 @@ class ChecklistTestCase(CourseTestCase): def test_get_checklists(self): """ Tests the get checklists method. """ - checklists_url = get_url_reverse('Checklists', self.course) + checklists_url = reverse("checklists", kwargs={ + 'org': self.course.location.org, + 'course': self.course.location.course, + 'name': self.course.location.name, + }) response = self.client.get(checklists_url) self.assertContains(response, "Getting Started With Studio") payload = response.content diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b15c05b984..4c9fcf7f81 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -95,8 +95,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): self.client.login(username=uname, password=password) def tearDown(self): - mongo = MongoClient() - mongo.drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) + MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db']) _CONTENTSTORE.clear() def check_components_on_page(self, component_types, expected_types): @@ -604,6 +603,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): 'org': 'MITx', 'number': '999', 'display_name': 'Robot Super Course', + 'run': '2013_Spring' } module_store = modulestore('direct') @@ -612,12 +612,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): 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') + self.assertEqual(data['id'], 'i4x://MITx/999/course/2013_Spring') content_store = contentstore() source_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') - dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course') + dest_location = CourseDescriptor.id_to_location('MITx/999/2013_Spring') clone_course(module_store, content_store, source_location, dest_location) @@ -855,6 +855,68 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): shutil.rmtree(root_dir) + def test_export_course_with_metadata_only_word_cloud(self): + """ + Similar to `test_export_course_with_metadata_only_video`. + """ + module_store = modulestore('direct') + draft_store = modulestore('draft') + content_store = contentstore() + + import_from_xml(module_store, 'common/test/data/', ['word_cloud']) + location = CourseDescriptor.id_to_location('HarvardX/ER22x/2013_Spring') + + verticals = module_store.get_items(['i4x', 'HarvardX', 'ER22x', 'vertical', None, None]) + + self.assertGreater(len(verticals), 0) + + parent = verticals[0] + + ItemFactory.create(parent_location=parent.location, category="word_cloud", display_name="untitled") + + root_dir = path(mkdtemp_clean()) + + print 'Exporting to tempdir = {0}'.format(root_dir) + + # export out to a tempdir + export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store) + + shutil.rmtree(root_dir) + + def test_empty_data_roundtrip(self): + """ + Test that an empty `data` field is preserved through + export/import. + """ + module_store = modulestore('direct') + draft_store = modulestore('draft') + content_store = contentstore() + + import_from_xml(module_store, 'common/test/data/', ['toy']) + location = CourseDescriptor.id_to_location('edX/toy/2012_Fall') + + verticals = module_store.get_items(['i4x', 'edX', 'toy', 'vertical', None, None]) + + self.assertGreater(len(verticals), 0) + + parent = verticals[0] + + # Create a module, and ensure that its `data` field is empty + word_cloud = ItemFactory.create(parent_location=parent.location, category="word_cloud", display_name="untitled") + del word_cloud.data + self.assertEquals(word_cloud.data, '') + + # Export the course + root_dir = path(mkdtemp_clean()) + export_to_xml(module_store, content_store, location, root_dir, 'test_roundtrip', draft_modulestore=draft_store) + + # Reimport and get the video back + import_from_xml(module_store, root_dir) + imported_word_cloud = module_store.get_item(Location(['i4x', 'edX', 'toy', 'word_cloud', 'untitled', None])) + + # It should now contain empty data + self.assertEquals(imported_word_cloud.data, '') + def test_course_handouts_rewrites(self): module_store = modulestore('direct') @@ -954,6 +1016,7 @@ class ContentStoreTest(ModuleStoreTestCase): 'org': 'MITx', 'number': '999', 'display_name': 'Robot Super Course', + 'run': '2013_Spring' } def tearDown(self): @@ -965,24 +1028,30 @@ class ContentStoreTest(ModuleStoreTestCase): """Test new course creation - happy path""" self.assert_created_course() - def assert_created_course(self): + def assert_created_course(self, number_suffix=None): """ Checks that the course was created properly. """ - resp = self.client.post(reverse('create_new_course'), self.course_data) + test_course_data = {} + test_course_data.update(self.course_data) + if number_suffix: + test_course_data['number'] = '{0}_{1}'.format(test_course_data['number'], number_suffix) + resp = self.client.post(reverse('create_new_course'), test_course_data) self.assertEqual(resp.status_code, 200) data = parse_json(resp) - self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') + self.assertNotIn('ErrMsg', data) + self.assertEqual(data['id'], 'i4x://MITx/{0}/course/2013_Spring'.format(test_course_data['number'])) + return test_course_data def test_create_course_check_forum_seeding(self): """Test new course creation and verify forum seeding """ - self.assert_created_course() - self.assertTrue(are_permissions_roles_seeded('MITx/999/Robot_Super_Course')) + test_course_data = self.assert_created_course(number_suffix=uuid4().hex) + self.assertTrue(are_permissions_roles_seeded('MITx/{0}/2013_Spring'.format(test_course_data['number']))) def test_create_course_duplicate_course(self): """Test new course creation - error path""" self.client.post(reverse('create_new_course'), self.course_data) - self.assert_course_creation_failed('There is already a course defined with this name.') + self.assert_course_creation_failed('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.') def assert_course_creation_failed(self, error_message): """ @@ -997,8 +1066,9 @@ class ContentStoreTest(ModuleStoreTestCase): """Test new course creation - error path""" self.client.post(reverse('create_new_course'), self.course_data) self.course_data['display_name'] = 'Robot Super Course Two' + self.course_data['run'] = '2013_Summer' - self.assert_course_creation_failed('There is already a course defined with the same organization and course number.') + self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change at least one field to be unique.') def test_create_course_with_bad_organization(self): """Test new course creation - error path for bad organization name""" @@ -1167,7 +1237,9 @@ class ContentStoreTest(ModuleStoreTestCase): # manage users resp = self.client.get(reverse('manage_users', - kwargs={'location': loc.url()})) + kwargs={'org': loc.org, + 'course': loc.course, + 'name': loc.name})) self.assertEqual(200, resp.status_code) # course info diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 0862eb462d..2007ba2f69 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -18,8 +18,6 @@ from xmodule.modulestore.tests.factories import CourseFactory from models.settings.course_metadata import CourseMetadata -from xmodule.modulestore.xml_importer import import_from_xml -from xmodule.modulestore.django import modulestore from xmodule.fields import Date from .utils import CourseTestCase @@ -167,8 +165,8 @@ class CourseDetailsViewTest(CourseTestCase): self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val)) @staticmethod - def convert_datetime_to_iso(dt): - return Date().to_json(dt) + def convert_datetime_to_iso(datetime_obj): + return Date().to_json(datetime_obj) def test_update_and_fetch(self): loc = self.course.location diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py index 30114496c8..c121b1bc09 100644 --- a/cms/djangoapps/contentstore/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/tests/test_course_updates.py @@ -2,6 +2,7 @@ from contentstore.tests.test_course_settings import CourseTestCase from django.core.urlresolvers import reverse import json +from xmodule.modulestore.django import modulestore class CourseUpdateTest(CourseTestCase): @@ -145,3 +146,36 @@ class CourseUpdateTest(CourseTestCase): resp = self.client.delete(url) payload = json.loads(resp.content) self.assertTrue(len(payload) == before_delete - 1) + + def test_no_ol_course_update(self): + '''Test trying to add to a saved course_update which is not an ol.''' + # get the updates and set to something wrong + location = self.course.location.replace(category='course_info', name='updates') + modulestore('direct').create_and_save_xmodule(location) + course_updates = modulestore('direct').get_item(location) + course_updates.data = 'bad news' + modulestore('direct').update_item(location, course_updates.data) + + init_content = '' + payload = {'content': content, + 'date': 'January 8, 2013'} + url = reverse('course_info_json', + 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) + + self.assertHTMLEqual(payload['content'], content) + + # now confirm that the bad news and the iframe make up 2 updates + url = reverse('course_info_json', + kwargs={'org': self.course.location.org, + 'course': self.course.location.course, + 'provided_id': ''}) + resp = self.client.get(url) + payload = json.loads(resp.content) + self.assertTrue(len(payload) == 2) diff --git a/cms/djangoapps/contentstore/tests/test_crud.py b/cms/djangoapps/contentstore/tests/test_crud.py index 84643f7787..e12711a6ff 100644 --- a/cms/djangoapps/contentstore/tests/test_crud.py +++ b/cms/djangoapps/contentstore/tests/test_crud.py @@ -127,7 +127,7 @@ class TemplateTests(unittest.TestCase): persistent_factories.ItemFactory.create(display_name='chapter 1', parent_location=test_course.location) - id_locator = CourseLocator(course_id=test_course.location.course_id, revision='draft') + id_locator = CourseLocator(course_id=test_course.location.course_id, branch='draft') guid_locator = CourseLocator(version_guid=test_course.location.version_guid) # verify it can be retireved by id self.assertIsInstance(modulestore('split').get_course(id_locator), CourseDescriptor) diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py index 88df19ec2d..e6baf57213 100644 --- a/cms/djangoapps/contentstore/tests/test_i18n.py +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -85,9 +85,11 @@ class InternationalizationTest(ModuleStoreTestCase): HTTP_ACCEPT_LANGUAGE='fr' ) - TEST_STRING = u'

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

      ' + TEST_STRING = ( + u'

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

      ' + ) self.assertContains(resp, TEST_STRING, diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py index 578b82b3cf..827dd1b054 100644 --- a/cms/djangoapps/contentstore/tests/test_item.py +++ b/cms/djangoapps/contentstore/tests/test_item.py @@ -14,19 +14,26 @@ class DeleteItem(CourseTestCase): super(DeleteItem, self).setUp() self.course = CourseFactory.create(org='mitX', number='333', display_name='Dummy Course') - def testDeleteStaticPage(self): + def test_delete_static_page(self): # Add static tab data = json.dumps({ 'parent_location': 'i4x://mitX/333/course/Dummy_Course', 'category': 'static_tab' }) - resp = self.client.post(reverse('create_item'), data, - content_type="application/json") + resp = self.client.post( + reverse('create_item'), + data, + content_type="application/json" + ) self.assertEqual(resp.status_code, 200) # Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore). - resp = self.client.post(reverse('delete_item'), resp.content, "application/json") + resp = self.client.post( + reverse('delete_item'), + resp.content, + "application/json" + ) self.assertEqual(resp.status_code, 200) @@ -122,6 +129,7 @@ class TestCreateItem(CourseTestCase): ) self.assertEqual(resp.status_code, 200) + class TestEditItem(CourseTestCase): """ Test contentstore.views.item.save_item @@ -151,10 +159,10 @@ class TestEditItem(CourseTestCase): chap_location = self.response_id(resp) resp = self.client.post( reverse('create_item'), - json.dumps( - {'parent_location': chap_location, - 'category': 'sequential' - }), + json.dumps({ + 'parent_location': chap_location, + 'category': 'sequential', + }), content_type="application/json" ) self.seq_location = self.response_id(resp) @@ -162,9 +170,10 @@ class TestEditItem(CourseTestCase): template_id = 'multiplechoice.yaml' resp = self.client.post( reverse('create_item'), - json.dumps({'parent_location': self.seq_location, - 'category': 'problem', - 'boilerplate': template_id + json.dumps({ + 'parent_location': self.seq_location, + 'category': 'problem', + 'boilerplate': template_id, }), content_type="application/json" ) @@ -195,7 +204,6 @@ class TestEditItem(CourseTestCase): problem = modulestore('draft').get_item(self.problems[0]) self.assertEqual(problem.rerandomize, 'never') - def test_null_field(self): """ Sending null in for a field 'deletes' it @@ -240,4 +248,3 @@ class TestEditItem(CourseTestCase): sequential = modulestore().get_item(self.seq_location) self.assertEqual(sequential.lms.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) self.assertEqual(sequential.lms.start, datetime.datetime(2010, 9, 12, 14, 0, tzinfo=UTC)) - diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py index 8fea4004dd..4b9dcf487f 100644 --- a/cms/djangoapps/contentstore/tests/test_users.py +++ b/cms/djangoapps/contentstore/tests/test_users.py @@ -1,195 +1,319 @@ -""" -Tests for user.py. -""" import json -import mock from .utils import CourseTestCase +from django.contrib.auth.models import User, Group from django.core.urlresolvers import reverse -from contentstore.views.user import _get_course_creator_status -from course_creators.views import add_user_with_status_granted -from course_creators.admin import CourseCreatorAdmin -from course_creators.models import CourseCreator - -from django.http import HttpRequest -from django.contrib.auth.models import User -from django.contrib.admin.sites import AdminSite +from auth.authz import get_course_groupname_for_role class UsersTestCase(CourseTestCase): def setUp(self): super(UsersTestCase, self).setUp() - self.url = reverse("add_user", kwargs={"location": ""}) + self.ext_user = User.objects.create_user( + "joe", "joe@comedycentral.com", "haha") + self.ext_user.is_active = True + self.ext_user.is_staff = False + self.ext_user.save() + self.inactive_user = User.objects.create_user( + "carl", "carl@comedycentral.com", "haha") + self.inactive_user.is_active = False + self.inactive_user.is_staff = False + self.inactive_user.save() - def test_empty(self): - resp = self.client.post(self.url) + self.index_url = reverse("manage_users", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + }) + self.detail_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": self.ext_user.email, + }) + self.inactive_detail_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": self.inactive_user.email, + }) + self.invalid_detail_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": "nonexistent@user.com", + }) + self.staff_groupname = get_course_groupname_for_role(self.course.location, "staff") + self.inst_groupname = get_course_groupname_for_role(self.course.location, "instructor") + + def test_index(self): + resp = self.client.get(self.index_url) + # ext_user is not currently a member of the course team, and so should + # not show up on the page. + self.assertNotContains(resp, self.ext_user.email) + + def test_index_member(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.get(self.index_url) + self.assertContains(resp, self.ext_user.email) + + def test_detail(self): + resp = self.client.get(self.detail_url) + self.assertEqual(resp.status_code, 200) + result = json.loads(resp.content) + self.assertEqual(result["role"], None) + self.assertTrue(result["active"]) + + def test_detail_inactive(self): + resp = self.client.get(self.inactive_detail_url) + self.assert2XX(resp.status_code) + result = json.loads(resp.content) + self.assertFalse(result["active"]) + + def test_detail_invalid(self): + resp = self.client.get(self.invalid_detail_url) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + + def test_detail_post(self): + resp = self.client.post( + self.detail_url, + data={"role": None}, + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + # no content: should not be in any roles + self.assertNotIn(self.staff_groupname, groups) + self.assertNotIn(self.inst_groupname, groups) + + def test_detail_post_staff(self): + resp = self.client.post( + self.detail_url, + data=json.dumps({"role": "staff"}), + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.staff_groupname, groups) + self.assertNotIn(self.inst_groupname, groups) + + def test_detail_post_staff_other_inst(self): + inst_group, _ = Group.objects.get_or_create(name=self.inst_groupname) + self.user.groups.add(inst_group) + self.user.save() + + resp = self.client.post( + self.detail_url, + data=json.dumps({"role": "staff"}), + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.staff_groupname, groups) + self.assertNotIn(self.inst_groupname, groups) + # check that other user is unchanged + user = User.objects.get(email=self.user.email) + groups = [g.name for g in user.groups.all()] + self.assertNotIn(self.staff_groupname, groups) + self.assertIn(self.inst_groupname, groups) + + def test_detail_post_instructor(self): + resp = self.client.post( + self.detail_url, + data=json.dumps({"role": "instructor"}), + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertNotIn(self.staff_groupname, groups) + self.assertIn(self.inst_groupname, groups) + + def test_detail_post_missing_role(self): + resp = self.client.post( + self.detail_url, + data=json.dumps({"toys": "fun"}), + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + + def test_detail_post_bad_json(self): + resp = self.client.post( + self.detail_url, + data="{foo}", + content_type="application/json", + HTTP_ACCEPT="application/json", + ) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + + def test_detail_post_no_json(self): + resp = self.client.post( + self.detail_url, + data={"role": "staff"}, + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.staff_groupname, groups) + self.assertNotIn(self.inst_groupname, groups) + + def test_detail_delete_staff(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.delete( + self.detail_url, + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertNotIn(self.staff_groupname, groups) + + def test_detail_delete_instructor(self): + group, _ = Group.objects.get_or_create(name=self.inst_groupname) + self.user.groups.add(group) + self.ext_user.groups.add(group) + self.user.save() + self.ext_user.save() + + resp = self.client.delete( + self.detail_url, + HTTP_ACCEPT="application/json", + ) + self.assert2XX(resp.status_code) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertNotIn(self.inst_groupname, groups) + + def test_delete_last_instructor(self): + group, _ = Group.objects.get_or_create(name=self.inst_groupname) + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.delete( + self.detail_url, + HTTP_ACCEPT="application/json", + ) self.assertEqual(resp.status_code, 400) - content = json.loads(resp.content) - self.assertEqual(content["Status"], "Failed") + result = json.loads(resp.content) + self.assertIn("error", result) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.inst_groupname, groups) + def test_post_last_instructor(self): + group, _ = Group.objects.get_or_create(name=self.inst_groupname) + self.ext_user.groups.add(group) + self.ext_user.save() -class IndexCourseCreatorTests(CourseTestCase): - """ - Tests the various permutations of course creator status. - """ - def setUp(self): - super(IndexCourseCreatorTests, self).setUp() + resp = self.client.post( + self.detail_url, + data={"role": "staff"}, + HTTP_ACCEPT="application/json", + ) + self.assertEqual(resp.status_code, 400) + result = json.loads(resp.content) + self.assertIn("error", result) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.inst_groupname, groups) - self.index_url = reverse("index") - self.request_access_url = reverse("request_course_creator") - - # Disable course creation takes precedence over enable creator group. I have enabled the - # latter to make this clear. - self.disable_course_creation = { - "DISABLE_COURSE_CREATION": True, - "ENABLE_CREATOR_GROUP": True, - 'STUDIO_REQUEST_EMAIL': 'mark@marky.mark', - } - - self.enable_creator_group = {"ENABLE_CREATOR_GROUP": True} - - self.admin = User.objects.create_user('Mark', 'mark+courses@edx.org', 'foo') - self.admin.is_staff = True - - def test_get_course_creator_status_disable_creation(self): - # DISABLE_COURSE_CREATION is True (this is the case on edx, where we have a marketing site). - # Only edx staff can create courses. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation): - self.assertTrue(self.user.is_staff) - self.assertEquals('granted', _get_course_creator_status(self.user)) - self._set_user_non_staff() - self.assertFalse(self.user.is_staff) - self.assertEquals('disallowed_for_this_site', _get_course_creator_status(self.user)) - - def test_get_course_creator_status_default_cause(self): - # Neither ENABLE_CREATOR_GROUP nor DISABLE_COURSE_CREATION are enabled. Anyone can create a course. - self.assertEquals('granted', _get_course_creator_status(self.user)) - self._set_user_non_staff() - self.assertEquals('granted', _get_course_creator_status(self.user)) - - def test_get_course_creator_status_creator_group(self): - # ENABLE_CREATOR_GROUP is True. This is the case on edge. - # Only staff members and users who have been granted access can create courses. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - # Staff members can always create courses. - self.assertEquals('granted', _get_course_creator_status(self.user)) - # Non-staff must request access. - self._set_user_non_staff() - self.assertEquals('unrequested', _get_course_creator_status(self.user)) - # Staff user requests access. - self.client.post(self.request_access_url) - self.assertEquals('pending', _get_course_creator_status(self.user)) - - def test_get_course_creator_status_creator_group_granted(self): - # ENABLE_CREATOR_GROUP is True. This is the case on edge. - # Check return value for a non-staff user who has been granted access. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - self._set_user_non_staff() - add_user_with_status_granted(self.admin, self.user) - self.assertEquals('granted', _get_course_creator_status(self.user)) - - def test_get_course_creator_status_creator_group_denied(self): - # ENABLE_CREATOR_GROUP is True. This is the case on edge. - # Check return value for a non-staff user who has been denied access. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - self._set_user_non_staff() - self._set_user_denied() - self.assertEquals('denied', _get_course_creator_status(self.user)) - - def test_disable_course_creation_enabled_non_staff(self): - # Test index page content when DISABLE_COURSE_CREATION is True, non-staff member. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation): - self._set_user_non_staff() - self._assert_cannot_create() - - def test_disable_course_creation_enabled_staff(self): - # Test index page content when DISABLE_COURSE_CREATION is True, staff member. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.disable_course_creation): - resp = self._assert_can_create() - self.assertFalse('Email staff to create course' in resp.content) - - def test_can_create_by_default(self): - # Test index page content with neither ENABLE_CREATOR_GROUP nor DISABLE_COURSE_CREATION enabled. - # Anyone can create a course. - self._assert_can_create() - self._set_user_non_staff() - self._assert_can_create() - - def test_course_creator_group_enabled(self): - # Test index page content with ENABLE_CREATOR_GROUP True. - # Staff can always create a course, others must request access. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - # Staff members can always create courses. - self._assert_can_create() - - # Non-staff case. - self._set_user_non_staff() - resp = self._assert_cannot_create() - self.assertTrue(self.request_access_url in resp.content) - - # Now request access. - self.client.post(self.request_access_url) - - # Still cannot create a course, but the "request access button" is no longer there. - resp = self._assert_cannot_create() - self.assertFalse(self.request_access_url in resp.content) - self.assertTrue('has-status is-pending' in resp.content) - - def test_course_creator_group_granted(self): - # Test index page content with ENABLE_CREATOR_GROUP True, non-staff member with access granted. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - self._set_user_non_staff() - add_user_with_status_granted(self.admin, self.user) - self._assert_can_create() - - def test_course_creator_group_denied(self): - # Test index page content with ENABLE_CREATOR_GROUP True, non-staff member with access denied. - with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group): - self._set_user_non_staff() - self._set_user_denied() - resp = self._assert_cannot_create() - self.assertFalse(self.request_access_url in resp.content) - self.assertTrue('has-status is-denied' in resp.content) - - def _assert_can_create(self): - """ - Helper method that posts to the index page and checks that the user can create a course. - - Returns the response from the post. - """ - resp = self.client.post(self.index_url) - self.assertTrue('new-course-button' in resp.content) - self.assertFalse(self.request_access_url in resp.content) - self.assertFalse('Email staff to create course' in resp.content) - return resp - - def _assert_cannot_create(self): - """ - Helper method that posts to the index page and checks that the user cannot create a course. - - Returns the response from the post. - """ - resp = self.client.post(self.index_url) - self.assertFalse('new-course-button' in resp.content) - return resp - - def _set_user_non_staff(self): - """ - Sets user as non-staff. - """ + def test_permission_denied_self(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.user.groups.add(group) self.user.is_staff = False self.user.save() - def _set_user_denied(self): - """ - Sets course creator status to denied in admin table. - """ - self.table_entry = CourseCreator(user=self.user) - self.table_entry.save() + self_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": self.user.email, + }) - self.deny_request = HttpRequest() - self.deny_request.user = self.admin + resp = self.client.post( + self_url, + data={"role": "instructor"}, + HTTP_ACCEPT="application/json", + ) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) - self.creator_admin = CourseCreatorAdmin(self.table_entry, AdminSite()) + def test_permission_denied_other(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.user.groups.add(group) + self.user.is_staff = False + self.user.save() - self.table_entry.state = CourseCreator.DENIED - self.creator_admin.save_model(self.deny_request, self.table_entry, None, True) + resp = self.client.post( + self.detail_url, + data={"role": "instructor"}, + HTTP_ACCEPT="application/json", + ) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + + def test_staff_can_delete_self(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.user.groups.add(group) + self.user.is_staff = False + self.user.save() + + self_url = reverse("course_team_user", kwargs={ + "org": self.course.location.org, + "course": self.course.location.course, + "name": self.course.location.name, + "email": self.user.email, + }) + + resp = self.client.delete(self_url) + self.assert2XX(resp.status_code) + # reload user from DB + user = User.objects.get(email=self.user.email) + groups = [g.name for g in user.groups.all()] + self.assertNotIn(self.staff_groupname, groups) + + def test_staff_cannot_delete_other(self): + group, _ = Group.objects.get_or_create(name=self.staff_groupname) + self.user.groups.add(group) + self.user.is_staff = False + self.user.save() + self.ext_user.groups.add(group) + self.ext_user.save() + + resp = self.client.delete(self.detail_url) + self.assert4XX(resp.status_code) + result = json.loads(resp.content) + self.assertIn("error", result) + # reload user from DB + ext_user = User.objects.get(email=self.ext_user.email) + groups = [g.name for g in ext_user.groups.all()] + self.assertIn(self.staff_groupname, groups) diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index fec82db1bb..26c49843b5 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -72,50 +72,6 @@ class LMSLinksTestCase(TestCase): ) -class UrlReverseTestCase(ModuleStoreTestCase): - """ Tests for get_url_reverse """ - def test_course_page_names(self): - """ Test the defined course pages. """ - course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course') - - self.assertEquals( - '/manage_users/i4x://mitX/666/course/URL_Reverse_Course', - utils.get_url_reverse('ManageUsers', course) - ) - - self.assertEquals( - '/mitX/666/settings-details/URL_Reverse_Course', - utils.get_url_reverse('SettingsDetails', course) - ) - - self.assertEquals( - '/mitX/666/settings-grading/URL_Reverse_Course', - utils.get_url_reverse('SettingsGrading', course) - ) - - self.assertEquals( - '/mitX/666/course/URL_Reverse_Course', - utils.get_url_reverse('CourseOutline', course) - ) - - self.assertEquals( - '/mitX/666/checklists/URL_Reverse_Course', - utils.get_url_reverse('Checklists', course) - ) - - def test_unknown_passes_through(self): - """ Test that unknown values pass through. """ - course = CourseFactory.create(org='mitX', number='666', display_name='URL Reverse Course') - self.assertEquals( - 'foobar', - utils.get_url_reverse('foobar', course) - ) - self.assertEquals( - 'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', - utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course) - ) - - class ExtraPanelTabTestCase(TestCase): """ Tests adding and removing extra course tabs. """ diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index d55a7eff55..1f2a4185a3 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -15,14 +15,16 @@ class ContentStoreTestCase(ModuleStoreTestCase): Login. View should always return 200. The success/fail is in the returned json """ - resp = self.client.post(reverse('login_post'), - {'email': email, 'password': password}) + resp = self.client.post( + reverse('login_post'), + {'email': email, 'password': password} + ) self.assertEqual(resp.status_code, 200) return resp - def login(self, email, pw): + def login(self, email, password): """Login, check that it worked.""" - resp = self._login(email, pw) + resp = self._login(email, password) data = parse_json(resp) self.assertTrue(data['success']) return resp @@ -178,11 +180,15 @@ class ForumTestCase(CourseTestCase): def test_blackouts(self): now = datetime.datetime.now(UTC) - self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in - [(now - datetime.timedelta(days=14), now - datetime.timedelta(days=11)), - (now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))]] + times1 = [ + (now - datetime.timedelta(days=14), now - datetime.timedelta(days=11)), + (now + datetime.timedelta(days=24), now + datetime.timedelta(days=30)) + ] + self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in times1] self.assertTrue(self.course.forum_posts_allowed) - self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in - [(now - datetime.timedelta(days=14), now + datetime.timedelta(days=2)), - (now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))]] + times2 = [ + (now - datetime.timedelta(days=14), now + datetime.timedelta(days=2)), + (now + datetime.timedelta(days=24), now + datetime.timedelta(days=30)) + ] + self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in times2] self.assertFalse(self.course.forum_posts_allowed) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 4973bddaca..a2e927ef46 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -188,38 +188,6 @@ def update_item(location, value): get_modulestore(location).update_item(location, value) -def get_url_reverse(course_page_name, course_module): - """ - Returns the course URL link to the specified location. This value is suitable to use as an href link. - - course_page_name should correspond to an attribute in CoursePageNames (for example, 'ManageUsers' - or 'SettingsDetails'), or else it will simply be returned. This method passes back unknown values of - course_page_names so that it can also be used for absolute (known) URLs. - - course_module is used to obtain the location, org, course, and name properties for a course, if - course_page_name corresponds to an attribute in CoursePageNames. - """ - url_name = getattr(CoursePageNames, course_page_name, None) - ctx_loc = course_module.location - - if CoursePageNames.ManageUsers == url_name: - return reverse(url_name, kwargs={"location": ctx_loc}) - elif url_name in [CoursePageNames.SettingsDetails, CoursePageNames.SettingsGrading, - CoursePageNames.CourseOutline, CoursePageNames.Checklists]: - return reverse(url_name, kwargs={'org': ctx_loc.org, 'course': ctx_loc.course, 'name': ctx_loc.name}) - else: - return course_page_name - - -class CoursePageNames: - """ Constants for pages that are recognized by get_url_reverse method. """ - ManageUsers = "manage_users" - SettingsDetails = "settings_details" - SettingsGrading = "settings_grading" - CourseOutline = "course_index" - Checklists = "checklists" - - def add_extra_panel_tab(tab_type, course): """ Used to add the panel tab to a course if it does not exist. diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 0bb9551ac9..e4201cddd7 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -29,7 +29,6 @@ from xmodule.util.date_utils import get_default_time_display from xmodule.modulestore import InvalidLocationError from xmodule.exceptions import NotFoundError -from ..utils import get_url_reverse from .access import get_location_and_verify_access from util.json_request import JsonResponse @@ -284,7 +283,7 @@ def import_course(request, org, course, name): tar_file.extractall(course_dir + '/') # find the 'course.xml' file - + dirpath = None for dirpath, _dirnames, filenames in os.walk(course_dir): for filename in filenames: if filename == 'course.xml': @@ -320,7 +319,11 @@ def import_course(request, org, course, name): return render_to_response('import.html', { 'context_course': course_module, - 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module) + 'successful_import_redirect_url': reverse('course_index', kwargs={ + 'org': location.org, + 'course': location.course, + 'name': location.name, + }) }) diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py index bcf4a1a5b9..74f0a33769 100644 --- a/cms/djangoapps/contentstore/views/checklist.py +++ b/cms/djangoapps/contentstore/views/checklist.py @@ -4,12 +4,13 @@ from util.json_request import JsonResponse from django.http import HttpResponseBadRequest from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_http_methods +from django.core.urlresolvers import reverse from django_future.csrf import ensure_csrf_cookie from mitxmako.shortcuts import render_to_response from xmodule.modulestore.inheritance import own_metadata -from ..utils import get_modulestore, get_url_reverse +from ..utils import get_modulestore from .access import get_location_and_verify_access from xmodule.course_module import CourseDescriptor @@ -96,10 +97,25 @@ def expand_checklist_action_urls(course_module): """ checklists = course_module.checklists modified = False + urlconf_map = { + "ManageUsers": "manage_users", + "SettingsDetails": "settings_details", + "SettingsGrading": "settings_grading", + "CourseOutline": "course_index", + "Checklists": "checklists", + } for checklist in checklists: if not checklist.get('action_urls_expanded', False): for item in checklist.get('items'): - item['action_url'] = get_url_reverse(item.get('action_url'), course_module) + action_url = item.get('action_url') + if action_url not in urlconf_map: + continue + urlconf_name = urlconf_map[action_url] + item['action_url'] = reverse(urlconf_name, kwargs={ + 'org': course_module.location.org, + 'course': course_module.location.course, + 'name': course_module.location.name, + }) checklist['action_urls_expanded'] = True modified = True diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 1be6ac2822..7cb503db1e 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -46,13 +46,19 @@ COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video'] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] NOTE_COMPONENT_TYPES = ['notes'] -ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud', 'videoalpha'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES +ADVANCED_COMPONENT_TYPES = [ + 'annotatable', + 'word_cloud', + 'videoalpha', + 'graphical_slider_tool' +] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' @login_required def edit_subsection(request, location): + "Edit the subsection of a course" # check that we have permissions to edit this item try: course = get_course_for_item(location) @@ -264,6 +270,7 @@ def assignment_type_update(request, org, course, category, name): @login_required @expect_json def create_draft(request): + "Create a draft" location = request.POST['id'] # check permissions for this user within this course @@ -280,6 +287,7 @@ def create_draft(request): @login_required @expect_json def publish_draft(request): + "Publish a draft" location = request.POST['id'] # check permissions for this user within this course @@ -295,6 +303,7 @@ def publish_draft(request): @login_required @expect_json def unpublish_unit(request): + "Unpublish a unit" location = request.POST['id'] # check permissions for this user within this course @@ -312,6 +321,7 @@ def unpublish_unit(request): @login_required @ensure_csrf_cookie def module_info(request, module_location): + "Get or set information for a module in the modulestore" location = Location(module_location) # check that logged in user has permissions to this item diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 02eb4c65b8..e68210dea4 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -3,6 +3,7 @@ Views related to operations on course objects """ import json import random +from django.utils.translation import ugettext as _ import string # pylint: disable=W0402 from django.contrib.auth.decorators import login_required @@ -101,12 +102,13 @@ def create_new_course(request): org = request.POST.get('org') number = request.POST.get('number') display_name = request.POST.get('display_name') + run = request.POST.get('run') try: - dest_location = Location('i4x', org, number, 'course', Location.clean(display_name)) + dest_location = Location('i4x', org, number, 'course', run) except InvalidLocationError as error: return JsonResponse({ - "ErrMsg": "Unable to create course '{name}'.\n\n{err}".format( + "ErrMsg": _("Unable to create course '{name}'.\n\n{err}").format( name=display_name, err=error.message)}) # see if the course already exists @@ -116,12 +118,24 @@ def create_new_course(request): except ItemNotFoundError: pass if existing_course is not None: - return JsonResponse({'ErrMsg': 'There is already a course defined with this name.'}) + return JsonResponse( + { + 'ErrMsg': _('There is already a course defined with the same organization, course number, and course run. Please change either organization or course number to be unique.'), + 'OrgErrMsg': _('Please change either the organization or course number so that it is unique.'), + 'CourseErrMsg': _('Please change either the organization or course number so that it is unique.'), + } + ) course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None] courses = modulestore().get_items(course_search_location) if len(courses) > 0: - return JsonResponse({'ErrMsg': 'There is already a course defined with the same organization and course number.'}) + return JsonResponse( + { + 'ErrMsg': _('There is already a course defined with the same organization and course number. Please change at least one field to be unique.'), + 'OrgErrMsg': _('Please change either the organization or course number so that it is unique.'), + 'CourseErrMsg': _('Please change either the organization or course number so that it is unique.'), + } + ) # instantiate the CourseDescriptor and then persist it # note: no system to pass diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 35af3e9ac3..f2a07abe32 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -68,6 +68,7 @@ def preview_dispatch(request, preview_id, location, dispatch=None): @login_required def preview_component(request, location): + "Return the HTML preview of a component" # TODO (vshnayder): change name from id to location in coffee+html as well. if not has_access(request.user, location): return HttpResponseForbidden() @@ -91,6 +92,7 @@ def preview_module_system(request, preview_id, descriptor): """ def preview_model_data(descriptor): + "Helper method to create a DbModel from a descriptor" return DbModel( SessionKeyValueStore(request, descriptor._model_data), descriptor.module_class, @@ -105,7 +107,7 @@ def preview_module_system(request, preview_id, descriptor): # TODO (cpennington): Do we want to track how instructors are using the preview problems? track_function=lambda event_type, event: None, filestore=descriptor.system.resources_fs, - get_module=partial(get_preview_module, request, preview_id), + get_module=partial(load_preview_module, request, preview_id), render_template=render_from_lms, debug=True, replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location), @@ -115,28 +117,13 @@ def preview_module_system(request, preview_id, descriptor): ) -def get_preview_module(request, preview_id, descriptor): - """ - Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily - from the set of preview data for the descriptor specified by Location - - request: The active django request - preview_id (str): An identifier specifying which preview this module is used for - location: A Location - """ - - return load_preview_module(request, preview_id, descriptor) - - def load_preview_module(request, preview_id, descriptor): """ - Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state + Return a preview XModule instantiated from the supplied descriptor. request: The active django request preview_id (str): An identifier specifying which preview this module is used for descriptor: An XModuleDescriptor - instance_state: An instance state string - shared_state: A shared state string """ system = preview_module_system(request, preview_id, descriptor) try: diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py index 0ee228b996..2f74df1d8c 100644 --- a/cms/djangoapps/contentstore/views/public.py +++ b/cms/djangoapps/contentstore/views/public.py @@ -1,3 +1,6 @@ +""" +Public views +""" from django_future.csrf import ensure_csrf_cookie from django.core.context_processors import csrf from django.shortcuts import redirect @@ -10,10 +13,6 @@ from .user import index __all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks'] -""" -Public views -""" - @ensure_csrf_cookie def signup(request): @@ -45,6 +44,7 @@ def login_page(request): def howitworks(request): + "Proxy view" if request.user.is_authenticated(): return index(request) else: diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py index 8a05bf1258..abbf84755e 100644 --- a/cms/djangoapps/contentstore/views/requests.py +++ b/cms/djangoapps/contentstore/views/requests.py @@ -1,4 +1,5 @@ from django.http import HttpResponse +from django.shortcuts import redirect from mitxmako.shortcuts import render_to_string, render_to_response __all__ = ['edge', 'event', 'landing'] @@ -11,7 +12,7 @@ def landing(request, org, course, coursename): # points to the temporary edge page def edge(request): - return render_to_response('university_profiles/edge.html', {}) + return redirect('/') def event(request): diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py index d55932e33d..f38685edfc 100644 --- a/cms/djangoapps/contentstore/views/tabs.py +++ b/cms/djangoapps/contentstore/views/tabs.py @@ -1,3 +1,6 @@ +""" +Views related to course tabs +""" from access import has_access from util.json_request import expect_json @@ -39,6 +42,7 @@ def initialize_course_tabs(course): @login_required @expect_json def reorder_static_tabs(request): + "Order the static tabs in the requested order" tabs = request.POST['tabs'] course = get_course_for_item(tabs[0]) @@ -86,6 +90,7 @@ def reorder_static_tabs(request): @login_required @ensure_csrf_cookie def edit_tabs(request, org, course, coursename): + "Edit tabs" location = ['i4x', org, course, 'course', coursename] store = get_modulestore(location) course_item = store.get_item(location) @@ -122,6 +127,7 @@ def edit_tabs(request, org, course, coursename): @login_required @ensure_csrf_cookie def static_pages(request, org, course, coursename): + "Static pages view" location = get_location_and_verify_access(request, org, course, coursename) diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py index 3a18448118..a0db8ecef8 100644 --- a/cms/djangoapps/contentstore/views/user.py +++ b/cms/djangoapps/contentstore/views/user.py @@ -1,7 +1,10 @@ +import json from django.conf import settings from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse +from django.contrib.auth.models import User, Group from django.contrib.auth.decorators import login_required +from django.views.decorators.http import require_http_methods from django.utils.translation import ugettext as _ from django.views.decorators.http import require_POST from django_future.csrf import ensure_csrf_cookie @@ -9,10 +12,13 @@ from mitxmako.shortcuts import render_to_response from django.core.context_processors import csrf from xmodule.modulestore.django import modulestore -from contentstore.utils import get_url_reverse, get_lms_link_for_item -from util.json_request import expect_json, JsonResponse -from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role -from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group +from xmodule.modulestore import Location +from contentstore.utils import get_lms_link_for_item +from util.json_request import JsonResponse +from auth.authz import ( + STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, + add_user_to_course_group, remove_user_from_course_group, + get_course_groupname_for_role) from course_creators.views import get_course_creator_status, add_user_with_status_unrequested, user_requested_access from .access import has_access @@ -36,11 +42,22 @@ def index(request): and course.location.name != '') courses = filter(course_filter, courses) + def format_course_for_view(course): + return ( + course.display_name, + reverse("course_index", kwargs={ + 'org': course.location.org, + 'course': course.location.course, + 'name': course.location.name, + }), + get_lms_link_for_item( + course.location, + course_id=course.location.course_id, + ), + ) + return render_to_response('index.html', { - 'courses': [(course.display_name, - get_url_reverse('CourseOutline', course), - get_lms_link_for_item(course.location, course_id=course.location.course_id)) - for course in courses], + 'courses': [format_course_for_view(c) for c in courses], 'user': request.user, 'request_course_creator_url': reverse('request_course_creator'), 'course_creator_status': _get_course_creator_status(request.user), @@ -60,104 +77,141 @@ def request_course_creator(request): @login_required @ensure_csrf_cookie -def manage_users(request, location): +def manage_users(request, org, course, name): ''' This view will return all CMS users who are editors for the specified course ''' + location = Location('i4x', org, course, 'course', name) # 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() course_module = modulestore().get_item(location) + staff_groupname = get_course_groupname_for_role(location, "staff") + staff_group, __ = Group.objects.get_or_create(name=staff_groupname) + inst_groupname = get_course_groupname_for_role(location, "instructor") + inst_group, __ = Group.objects.get_or_create(name=inst_groupname) + return render_to_response('manage_users.html', { '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('/'), + 'staff': staff_group.user_set.all(), + 'instructors': inst_group.user_set.all(), 'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME), - 'request_user_id': request.user.id }) -@expect_json @login_required @ensure_csrf_cookie -def add_user(request, location): - ''' - This POST-back view will add a user - specified by email - to the list of editors for - the specified course - ''' - email = request.POST.get("email") - - if not email: +@require_http_methods(("GET", "POST", "PUT", "DELETE")) +def course_team_user(request, org, course, name, email): + location = Location('i4x', org, course, 'course', name) + # check that logged in user has permissions to this item + if has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): + # instructors have full permissions + pass + elif has_access(request.user, location, role=STAFF_ROLE_NAME) and email == request.user.email: + # staff can only affect themselves + pass + else: msg = { - 'Status': 'Failed', - 'ErrMsg': _('Please specify an email address.'), + "error": _("Insufficient permissions") } return JsonResponse(msg, 400) - # remove leading/trailing whitespace if necessary - email = email.strip() - - # 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: + try: + user = User.objects.get(email=email) + except: msg = { - 'Status': 'Failed', - 'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email), + "error": _("Could not find user by email address '{email}'.").format(email=email), } return JsonResponse(msg, 404) - # user exists, but hasn't activated account?!? + # role hierarchy: "instructor" has more permissions than "staff" (in a course) + roles = ["instructor", "staff"] + + if request.method == "GET": + # just return info about the user + msg = { + "email": user.email, + "active": user.is_active, + "role": None, + } + # what's the highest role that this user has? + groupnames = set(g.name for g in user.groups.all()) + for role in roles: + role_groupname = get_course_groupname_for_role(location, role) + if role_groupname in groupnames: + msg["role"] = role + break + return JsonResponse(msg) + + # can't modify an inactive user if not user.is_active: msg = { - 'Status': 'Failed', - 'ErrMsg': _('User {email} has registered but has not yet activated his/her account.').format(email=email), + "error": _('User {email} has registered but has not yet activated his/her account.').format(email=email), } return JsonResponse(msg, 400) - # ok, we're cool to add to the course group - add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME) + # make sure that the role groups exist + staff_groupname = get_course_groupname_for_role(location, "staff") + staff_group, __ = Group.objects.get_or_create(name=staff_groupname) + inst_groupname = get_course_groupname_for_role(location, "instructor") + inst_group, __ = Group.objects.get_or_create(name=inst_groupname) - return JsonResponse({"Status": "OK"}) + if request.method == "DELETE": + # remove all roles in this course from this user: but fail if the user + # is the last instructor in the course team + instructors = set(inst_group.user_set.all()) + staff = set(staff_group.user_set.all()) + if user in instructors and len(instructors) == 1: + msg = { + "error": _("You may not remove the last instructor from a course") + } + return JsonResponse(msg, 400) + if user in instructors: + user.groups.remove(inst_group) + if user in staff: + user.groups.remove(staff_group) + user.save() + return JsonResponse() -@expect_json -@login_required -@ensure_csrf_cookie -def remove_user(request, location): - ''' - This POST-back view will remove a user - specified by email - from the list of editors for - the specified course - ''' + # all other operations require the requesting user to specify a role + if request.META.get("CONTENT_TYPE", "") == "application/json" and request.body: + try: + payload = json.loads(request.body) + except: + return JsonResponse({"error": _("malformed JSON")}, 400) + try: + role = payload["role"] + except KeyError: + return JsonResponse({"error": _("`role` is required")}, 400) + else: + if not "role" in request.POST: + return JsonResponse({"error": _("`role` is required")}, 400) + role = request.POST["role"] - 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() - - user = get_user_by_email(email) - if user is None: - msg = { - 'Status': 'Failed', - 'ErrMsg': _("Could not find user by email address '{email}'.").format(email=email), - } - return JsonResponse(msg, 404) - - # make sure we're not removing ourselves - if user.id == request.user.id: - raise PermissionDenied() - - remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME) - - return JsonResponse({"Status": "OK"}) + if role == "instructor": + if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME): + msg = { + "error": _("Only instructors may create other instructors") + } + return JsonResponse(msg, 400) + add_user_to_course_group(request.user, user, location, role) + elif role == "staff": + # if we're trying to downgrade a user from "instructor" to "staff", + # make sure we have at least one other instructor in the course team. + instructors = set(inst_group.user_set.all()) + if user in instructors: + if len(instructors) == 1: + msg = { + "error": _("You may not remove the last instructor from a course") + } + return JsonResponse(msg, 400) + remove_user_from_course_group(request.user, user, location, "instructor") + add_user_to_course_group(request.user, user, location, role) + return JsonResponse() def _get_course_creator_status(user): diff --git a/cms/envs/aws_migrate.py b/cms/envs/aws_migrate.py new file mode 100644 index 0000000000..54ced39c7c --- /dev/null +++ b/cms/envs/aws_migrate.py @@ -0,0 +1,24 @@ +""" +A Django settings file for use on AWS while running +database migrations, since we don't want to normally run the +LMS with enough privileges to modify the database schema. +""" + +# We intentionally define lots of variables that aren't used, and +# want to import all variables from base settings files +# pylint: disable=W0401, W0614 + +# Import everything from .aws so that our settings are based on those. +from .aws import * +import os +from django.core.exceptions import ImproperlyConfigured + +USER = os.environ.get('DB_MIGRATION_USER', 'root') +PASSWORD = os.environ.get('DB_MIGRATION_PASS', None) + +if not PASSWORD: + raise ImproperlyConfigured("No database password was provided for running " + "migrations. This is fatal.") + +DATABASES['default']['USER'] = USER +DATABASES['default']['PASSWORD'] = PASSWORD diff --git a/cms/static/js/base.js b/cms/static/js/base.js index ebd7ed95cb..de0fd955dc 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -597,11 +597,9 @@ function cancelNewSection(e) { function addNewCourse(e) { e.preventDefault(); - $('.new-course-button').addClass('disabled'); - $(e.target).addClass('disabled'); - var $newCourse = $($('#new-course-template').html()); + $('.new-course-button').addClass('is-disabled'); + var $newCourse = $('.wrapper-create-course').addClass('is-shown'); var $cancelButton = $newCourse.find('.new-course-cancel'); - $('.courses').prepend($newCourse); $newCourse.find('.new-course-name').focus().select(); $newCourse.find('form').bind('submit', saveNewCourse); $cancelButton.bind('click', cancelNewCourse); @@ -613,41 +611,97 @@ function addNewCourse(e) { function saveNewCourse(e) { e.preventDefault(); - var $newCourse = $(this).closest('.new-course'); - var org = $newCourse.find('.new-course-org').val(); - var number = $newCourse.find('.new-course-number').val(); - var display_name = $newCourse.find('.new-course-name').val(); + var $newCourseForm = $(this).closest('#create-course-form'); + var display_name = $newCourseForm.find('.new-course-name').val(); + var org = $newCourseForm.find('.new-course-org').val(); + var number = $newCourseForm.find('.new-course-number').val(); + var run = $newCourseForm.find('.new-course-run').val(); - if (org == '' || number == '' || display_name == '') { - alert(gettext('You must specify all fields in order to create a new course.')); - return; + var required_field_text = gettext('Required field'); + + var display_name_errMsg = (display_name === '') ? required_field_text : null; + var org_errMsg = (org === '') ? required_field_text : null; + var number_errMsg = (number === '') ? required_field_text : null; + var run_errMsg = (run === '') ? required_field_text : null; + + var bInErr = (display_name_errMsg || org_errMsg || number_errMsg || run_errMsg); + + // check for suitable encoding + if (!bInErr) { + var encoding_errMsg = gettext('Please do not use any spaces or special characters in this field.'); + + if (encodeURIComponent(org) != org) + org_errMsg = encoding_errMsg; + if (encodeURIComponent(number) != number) + number_errMsg = encoding_errMsg; + if (encodeURIComponent(run) != run) + run_errMsg = encoding_errMsg; + + bInErr = (org_errMsg || number_errMsg || run_errMsg); } + var header_err_msg = (bInErr) ? gettext('Please correct the fields below.') : null; + + var setNewCourseErrMsgs = function(header_err_msg, display_name_errMsg, org_errMsg, number_errMsg, run_errMsg) { + if (header_err_msg) { + $('.wrapper-create-course').addClass('has-errors'); + $('.wrap-error').addClass('is-shown'); + $('#course_creation_error').html('

      ' + header_err_msg + '

      '); + } else { + $('.wrap-error').removeClass('is-shown'); + $('#course_creation_error').html(''); + } + + var setNewCourseFieldInErr = function(el, msg) { + el.children('.tip-error').remove(); + if (msg !== null && msg !== '') { + el.addClass('error'); + el.append('' + msg + ''); + } else { + el.removeClass('error'); + } + }; + + setNewCourseFieldInErr($('#field-course-name'), display_name_errMsg); + setNewCourseFieldInErr($('#field-organization'), org_errMsg); + setNewCourseFieldInErr($('#field-course-number'), number_errMsg); + setNewCourseFieldInErr($('#field-course-run'), run_errMsg); + }; + + setNewCourseErrMsgs(header_err_msg, display_name_errMsg, org_errMsg, number_errMsg, run_errMsg); + + if (bInErr) + return; + analytics.track('Created a Course', { 'org': org, 'number': number, - 'display_name': display_name + 'display_name': display_name, + 'run': run }); $.post('/create_new_course', { - 'org': org, - 'number': number, - 'display_name': display_name - }, - - function(data) { - if (data.id != undefined) { - window.location = '/' + data.id.replace(/.*:\/\//, ''); - } else if (data.ErrMsg != undefined) { - alert(data.ErrMsg); + 'org': org, + 'number': number, + 'display_name': display_name, + 'run': run + }, + function(data) { + if (data.id !== undefined) { + window.location = '/' + data.id.replace(/.*:\/\//, ''); + } else if (data.ErrMsg !== undefined) { + var orgErrMsg = (data.OrgErrMsg !== undefined) ? data.OrgErrMsg : null; + var courseErrMsg = (data.CourseErrMsg !== undefined) ? data.CourseErrMsg : null; + setNewCourseErrMsgs(data.ErrMsg, null, orgErrMsg, courseErrMsg, null); + } } - }); + ); } function cancelNewCourse(e) { e.preventDefault(); - $('.new-course-button').removeClass('disabled'); - $(this).parents('section.new-course').remove(); + $('.new-course-button').removeClass('is-disabled'); + $('.wrapper-create-course').removeClass('is-shown'); } function addNewSubsection(e) { diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js index ecd9ebe78d..38d2c78576 100644 --- a/cms/static/js/views/course_info_edit.js +++ b/cms/static/js/views/course_info_edit.js @@ -34,16 +34,8 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ }, initialize: function() { - var self = this; - // instantiates an editor template for each update in the collection - window.templateLoader.loadRemoteTemplate("course_info_update", - // TODO Where should the template reside? how to use the static.url to create the path? - "/static/client_templates/course_info_update.html", - function (raw_template) { - self.template = _.template(raw_template); - self.render(); - } - ); + this.template = _.template($("#course_info_update-tpl").text()); + this.render(); // when the client refetches the updates as a whole, re-render them this.listenTo(this.collection, 'reset', this.render); }, @@ -241,16 +233,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ }, initialize: function() { + this.template = _.template($("#course_info_handouts-tpl").text()); var self = this; this.model.fetch({ complete: function() { - window.templateLoader.loadRemoteTemplate("course_info_handouts", - "/static/client_templates/course_info_handouts.html", - function (raw_template) { - self.template = _.template(raw_template); - self.render(); - } - ); + self.render(); }, reset: true }); diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js index 42ed2d6920..e41a6971a6 100644 --- a/cms/static/js/views/overview.js +++ b/cms/static/js/views/overview.js @@ -225,12 +225,19 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) { ui.draggable.attr("style", "position:relative;"); // STYLE hack too children.push(ui.draggable.data('id')); } + var saving = new CMS.Views.Notification.Mini({ + title: gettext('Saving') + '…' + }); + saving.show(); $.ajax({ url: "/save_item", type: "POST", dataType: "json", contentType: "application/json", - data:JSON.stringify({ 'id' : subsection_id, 'children' : children}) + data:JSON.stringify({ 'id' : subsection_id, 'children' : children}), + success: function() { + saving.hide(); + } }); } diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js index 90e84adf2b..5ae0c19570 100644 --- a/cms/static/js/views/settings/advanced_view.js +++ b/cms/static/js/views/settings/advanced_view.js @@ -11,16 +11,9 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ // TODO enable/disable save based on validation (currently enabled whenever there are changes) }, initialize : function() { - var self = this; - // instantiates an editor template for each update in the collection - window.templateLoader.loadRemoteTemplate("advanced_entry", - "/static/client_templates/advanced_entry.html", - function (raw_template) { - self.template = _.template(raw_template); - self.render(); - } - ); + this.template = _.template($("#advanced_entry-tpl").text()); this.listenTo(this.model, 'invalid', this.handleValidationError); + this.render(); }, render: function() { // catch potential outside call before template loaded @@ -56,7 +49,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ CodeMirror.fromTextArea(textarea, { mode: "application/json", lineNumbers: false, lineWrapping: false, onChange: function(instance, changeobj) { - instance.save() + instance.save(); // this event's being called even when there's no change :-( if (instance.getValue() !== oldValue) { var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented."); @@ -105,8 +98,7 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ // call validateKey on each to ensure proper format // check for dupes var self = this; - this.model.save({}, - { + this.model.save({}, { success : function() { self.render(); var title = gettext("Your policy changes have been saved."); diff --git a/cms/static/sass/_shame.scss b/cms/static/sass/_shame.scss index f9030b72e8..85d133af1f 100644 --- a/cms/static/sass/_shame.scss +++ b/cms/static/sass/_shame.scss @@ -23,6 +23,13 @@ body.dashboard { } +// yes we have no boldness today - need to fix the resets +body strong, +body b { + font-weight: 700; +} + +// known things to do (paint the fence, sand the floor, wax on/off) // ==================== diff --git a/cms/static/sass/elements/_forms.scss b/cms/static/sass/elements/_forms.scss index 9907b05995..c78e2f3692 100644 --- a/cms/static/sass/elements/_forms.scss +++ b/cms/static/sass/elements/_forms.scss @@ -93,6 +93,227 @@ form { } } +// ELEM: form wrapper +.wrapper-create-element { + height: 0; + margin-bottom: $baseline; + opacity: 0.0; + pointer-events: none; + overflow: hidden; + + &.animate { + @include transition(opacity $tmg-f1 ease-in-out 0s, height $tmg-f1 ease-in-out 0s); + } + + &.is-shown { + height: auto; // define a specific height for the animating version of this UI to work properly + opacity: 1.0; + pointer-events: auto; + } +} + +// ELEM: form +// form styling for creating a new content item (course, user, textbook) +form[class^="create-"] { + @extend .ui-window; + + .title { + @extend .t-title4; + font-weight: 600; + padding: $baseline ($baseline*1.5) 0 ($baseline*1.5); + } + + fieldset { + padding: $baseline ($baseline*1.5); + } + + + .list-input { + @extend .cont-no-list; + + .field { + margin: 0 0 ($baseline*0.75) 0; + + &:last-child { + margin-bottom: 0; + } + + &.required { + + label { + font-weight: 600; + } + + label:after { + margin-left: ($baseline/4); + content: "*"; + } + } + + label, input, textarea { + display: block; + } + + label { + @extend .t-copy-sub1; + @include transition(color $tmg-f3 ease-in-out 0s); + margin: 0 0 ($baseline/4) 0; + + &.is-focused { + color: $blue; + } + } + + + input, textarea { + @extend .t-copy-base; + @include transition(all $tmg-f2 ease-in-out 0s); + height: 100%; + width: 100%; + padding: ($baseline/2); + + &.long { + width: 100%; + } + + &.short { + width: 25%; + } + + /*@include placeholder { + color: $gray-l3; + }*/ + + &:focus { + + + .tip { + color: $gray; + } + } + } + + textarea.long { + height: ($baseline*5); + } + + input[type="checkbox"] { + display: inline-block; + margin-right: ($baseline/4); + width: auto; + height: auto; + + & + label { + display: inline-block; + } + } + + .tip { + @extend .t-copy-sub2; + @include transition(color, 0.15s, ease-in-out); + display: block; + margin-top: ($baseline/4); + color: $gray-l3; + } + + .tip-error { + display: none; + float: none; + } + + &.error { + label { + color: $red; + } + + .tip-error { + @extend .anim-fadeIn; + display: block; + color: $red; + } + + input { + border-color: $red; + } + } + } + + .field-inline { + + input, textarea, select { + width: 62%; + display: inline-block; + } + + .tip-stacked { + display: inline-block; + float: right; + width: 35%; + margin-top: 0; + } + + &.error { + .tip-error { + } + } + + } + + .field-group { + @include clearfix(); + margin: 0 0 ($baseline/2) 0; + + .field { + display: block; + width: 47%; + border-bottom: none; + margin: 0 ($baseline*0.75) 0 0; + padding: ($baseline/4) 0 0 0; + float: left; + position: relative; + + &:nth-child(odd) { + float: left; + } + + &:nth-child(even) { + float: right; + margin-right: 0; + } + + input, textarea { + width: 100%; + } + } + } + } + + .actions { + box-shadow: inset 0 1px 2px $shadow; + margin-top: ($baseline*0.75); + border-top: 1px solid $gray-l1; + padding: ($baseline*0.75) ($baseline*1.5); + background: $gray-l6; + + .action { + @include transition(all $tmg-f2 linear 0s); + display: inline-block; + padding: ($baseline/5) $baseline; + font-weight: 600; + text-transform: uppercase; + } + + .action-primary { + @include blue-button; + @extend .t-action2; + } + + .action-secondary { + @include grey-button; + @extend .t-action2; + } + } +} + // ==================== // forms - grandfathered diff --git a/cms/static/sass/elements/_icons.scss b/cms/static/sass/elements/_icons.scss index a75c97ea76..9bbb72d67e 100644 --- a/cms/static/sass/elements/_icons.scss +++ b/cms/static/sass/elements/_icons.scss @@ -1,4 +1,4 @@ -// studio - elements - icons +// studio - elements - icons & badges // ==================== .icon { @@ -14,3 +14,45 @@ vertical-align: middle; margin-right: ($baseline/4); } + +// ui - badges +.wrapper-ui-badge { + position: absolute; + top: -1px; + left: ($baseline*1.5); + width: 100%; +} + +.ui-badge { + @extend .t-title9; + position: relative; + border-bottom-right-radius: ($baseline/10); + border-bottom-left-radius: ($baseline/10); + padding: ($baseline/4) ($baseline/2) ($baseline/4) ($baseline/2); + font-weight: 600; + text-transform: uppercase; + + * [class^="icon-"] { + margin-right: ($baseline/5); + } + + // OPTION: add this class for a visual hanging display + &.is-hanging { + @include box-sizing(border-box); + @extend .ui-depth2; + top: -($baseline/4); + + &:after { + position: absolute; + top: 0; + right: -($baseline/4); + display: block; + height: 0; + width: 0; + border-bottom: ($baseline/4) solid $black-t3; + border-right: ($baseline/4) solid transparent; + content: ""; + opacity: 0.5; + } + } +} diff --git a/cms/static/sass/elements/_navigation.scss b/cms/static/sass/elements/_navigation.scss index d05965d83c..739d091b8e 100644 --- a/cms/static/sass/elements/_navigation.scss +++ b/cms/static/sass/elements/_navigation.scss @@ -64,12 +64,14 @@ nav { opacity: 0.0; pointer-events: none; width: ($baseline*8); + overflow: hidden; // dropped down state &.is-shown { opacity: 1.0; pointer-events: auto; + overflow: visible; } } diff --git a/cms/static/sass/elements/_system-help.scss b/cms/static/sass/elements/_system-help.scss index 3b33946e19..c5fcc6a0ec 100644 --- a/cms/static/sass/elements/_system-help.scss +++ b/cms/static/sass/elements/_system-help.scss @@ -55,8 +55,8 @@ margin-bottom: $baseline; .title { - @extend .t-title7; - margin-bottom: ($baseline/4); + @extend .t-title6; + margin-bottom: ($baseline/2); font-weight: 700; } @@ -167,6 +167,34 @@ } } +// particular notice - create +.notice-create { + background-color: $gray-l4; + + .title { + color: $gray-d2; + } + + .copy { + color: $gray-d2; + } + + &.has-actions { + + .list-actions { + + .action-item { + + } + + .action-primary { + @extend .btn-primary-green; + @extend .t-action3; + } + } + } +} + // particular notice - confirmation .notice-confirmation { background-color: $green-l5; diff --git a/cms/static/sass/views/_dashboard.scss b/cms/static/sass/views/_dashboard.scss index 63c8ab36fd..ab3ad6f810 100644 --- a/cms/static/sass/views/_dashboard.scss +++ b/cms/static/sass/views/_dashboard.scss @@ -358,22 +358,30 @@ body.dashboard { } } - .new-course { - @include clearfix(); - padding: ($baseline*0.75) ($baseline*1.25); - margin-top: $baseline; - border-radius: 3px; - border: 1px solid $gray; - background: $white; - box-shadow: 0 1px 2px rgba(0, 0, 0, .1); - .title { - @extend .t-title4; - font-weight: 600; - margin-bottom: ($baseline/2); - border-bottom: 1px solid $gray-l3; - padding-bottom: ($baseline/2); - } + // ELEM: new user form + .wrapper-create-course { + + // CASE: when form is animating + &.animate { + + // STATE: shown + &.is-shown { + height: ($baseline*26); + + // STATE: errors + &.has-errors { + height: ($baseline*33); + } + } + } + } + + // ==================== + + // course listings + + .create-course { .row { @include clearfix(); @@ -389,10 +397,6 @@ body.dashboard { margin-right: 4%; } - .course-info { - width: 600px; - } - label { @extend .t-title7; display: block; @@ -401,7 +405,8 @@ body.dashboard { .new-course-org, .new-course-number, - .new-course-name { + .new-course-name, + .new-course-run { width: 100%; } @@ -421,5 +426,25 @@ body.dashboard { .item-details { padding-bottom: 0; } + + .wrap-error { + @include transition(all $tmg-f2 ease 0s); + height: 0; + overflow: hidden; + opacity: 0; + } + + .wrap-error.is-shown { + height: 65px; + opacity: 1; + } + + .message-status { + display: block; + margin-bottom: 0; + padding: ($baseline*.5) ($baseline*1.5) 8px ($baseline*1.5); + font-weight: bold; + } + } } diff --git a/cms/static/sass/views/_textbooks.scss b/cms/static/sass/views/_textbooks.scss index 8d2b2d9489..8058673b2b 100644 --- a/cms/static/sass/views/_textbooks.scss +++ b/cms/static/sass/views/_textbooks.scss @@ -30,7 +30,7 @@ body.course.textbooks { } .textbook { - @extend .window; + @extend .ui-window; position: relative; .view-textbook { diff --git a/cms/static/sass/views/_users.scss b/cms/static/sass/views/_users.scss index ecaa319707..7e88edd38c 100644 --- a/cms/static/sass/views/_users.scss +++ b/cms/static/sass/views/_users.scss @@ -3,80 +3,227 @@ body.course.users { - .new-user-form { - display: none; - padding: 15px 20px; - background-color: $lightBluishGrey2; + // LAYOUT: page + .content-primary, .content-supplementary { + @include box-sizing(border-box); + float: left; + } - #result { - display: none; - float: left; - margin-bottom: 15px; - padding: 3px 15px; - border-radius: 3px; - background: $error-red; - font-size: 14px; - color: #fff; - } + .content-primary { + width: flex-grid(9, 12); + margin-right: flex-gutter(); + } - .form-elements { - clear: both; - } + .content-supplementary { + width: flex-grid(3, 12); + } - label { - display: inline-block; - margin-right: 10px; - } + // ELEM: content + .content { - .email-input { - width: 350px; - padding: 8px 8px 10px; - border-color: $darkGrey; + .introduction { + @extend .t-copy-sub1; + margin: 0 0 ($baseline*2) 0; } + } - .add-button { - @include blue-button; - padding: 5px 20px 9px; - } + // ELEM: no users notice + .content .notice-create { + width: flexgrid(9, 9); + margin-top: $baseline; - .cancel-button { - @include white-button; - padding: 5px 20px 9px; + // CASE: notice has actions { + &.has-actions { + + .msg, .list-actions { + display: inline-block; + vertical-align: middle; + } + + .msg { + width: flex-grid(6, 9); + margin-right: flex-gutter(); + } + + .list-actions { + width: flex-grid(3, 9); + text-align: right; + margin-top: 0; + + .action-item { + + } + + .action-primary { + @include green-button(); // overwriting for the sake of syncing older green button styles for now + @extend .t-action3; + padding: ($baseline/2) $baseline; + } + } } } + + // ELEM: new user form + .wrapper-create-user { + + &.is-shown { + height: ($baseline*15); + } + } + + // ELEM: listing of users + .user-list, .user-item, .item-metadata, .item-actions { + @include box-sizing(border-box); + } + .user-list { - border: 1px solid $mediumGrey; - background: #fff; - li { + .user-item { + @extend .ui-window; + @include clearfix(); position: relative; - padding: 20px; - border-bottom: 1px solid $mediumGrey; + width: flex-grid(9, 9); + margin: 0 0 ($baseline/2) 0; + padding: ($baseline*1.25) ($baseline*1.5) $baseline ($baseline*1.5); &:last-child { - border-bottom: none; + margin-bottom: 0; } - span { + .item-metadata, .item-actions { display: inline-block; + vertical-align: middle; } - .user-name { - margin-right: 10px; - font-size: 24px; - font-weight: 300; + // ELEM: item - flag + .flag-role { + @extend .ui-badge; + color: $white; + + .msg-you { + margin-left: ($baseline/5); + text-transform: none; + font-weight: 500; + color: $pink-l3; + } + + &:after { + border-bottom-color: $pink-d4; + } + + &.flag-role-staff { + background: $pink-u3; + } + + &.flag-role-admin { + background: $pink; + } } - .user-email { - font-size: 14px; - font-style: italic; - color: $mediumGrey; + // ELEM: item - metadata + .item-metadata { + width: flex-grid(5, 9); + margin-right: flex-gutter(); + + .user-username, .user-email { + display: inline-block; + vertical-align: middle; + } + + .user-username { + @extend .t-title4; + @include transition(color $tmg-f2 ease-in-out 0s); + margin: 0 ($baseline/2) ($baseline/10) 0; + color: $gray-d4; + font-weight: 600; + } + + .user-email { + @extend .t-title6; + } } + // ELEM: item - actions .item-actions { - top: 24px; + width: flex-grid(4, 9); + position: static; // nasty reset needed due to base.scss + text-align: right; + + .action { + display: inline-block; + vertical-align: middle; + } + + .action-role { + width: flex-grid(3, 4); + margin-right: flex-gutter(); + } + + .action-delete { + width: flex-grid(1, 4); + + // STATE: disabled + &.is-disabled { + opacity: 0.0; + visibility: hidden; + pointer-events: none; + } + } + + .delete { + @extend .ui-btn-non; + } + + // HACK: nasty reset needed due to base.scss + .delete-button { + margin-right: 0; + float: none; + color: inherit; + } + + // ELEM: admin role controls + .toggle-admin-role { + + &.add-admin-role { + @include blue-button; + @extend .t-action2; + @include transition(all .15s); + display: inline-block; + padding: ($baseline/5) $baseline; + font-weight: 600; + } + + &.remove-admin-role { + @include grey-button; + @extend .t-action2; + @include transition(all .15s); + display: inline-block; + padding: ($baseline/5) $baseline; + font-weight: 600; + } + } + + .notoggleforyou { + @extend .t-copy-sub1; + color: $gray-l2; + } + } + + // STATE: hover + &:hover { + + .user-username { + } + + .user-email { + + } + + .item-actions { + + } } } } -} \ No newline at end of file +} diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index dcfffd1d5a..03f4c35d14 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -6,9 +6,15 @@ <%block name="title">${_("Course Updates")} <%block name="bodyclass">is-signedin course course-info updates +<%block name="header_extras"> +% for template_name in ["course_info_update", "course_info_handouts"]: + +% endfor + <%block name="jsextra"> - diff --git a/cms/templates/index.html b/cms/templates/index.html index 53c744c780..9c845ccb5a 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -36,36 +36,6 @@ -<%block name="header_extras"> - - - <%block name="content">
      @@ -109,6 +79,57 @@ %endif
      + % if course_creator_status=='granted': +
      +
      +
      + +
      + +
      +

      ${_("Create a New Course")}

      + +
      + ${_("Required Information to Create a New Course")} + +
        +
      1. + + + ${_("The public display name for your course.")} +
      2. +
      3. + + + ${_("The name of the organization sponsoring the course")} - ${_("Note: No spaces or special characters are allowed. This cannot be changed.")} +
      4. + +
      5. + + + ${_("The unique number that identifies your course within your organization")} - ${_("Note: No spaces or special characters are allowed. This cannot be changed.")} +
      6. + +
      7. + + + ${_("The term in which your course will run")} - ${_("Note: No spaces or special characters are allowed. This cannot be changed.")} +
      8. +
      + +
      +
      + +
      + + +
      +
      +
      + % endif + %if len(courses) > 0:
        diff --git a/cms/templates/js/advanced_entry.underscore b/cms/templates/js/advanced_entry.underscore new file mode 100644 index 0000000000..26b1a386f6 --- /dev/null +++ b/cms/templates/js/advanced_entry.underscore @@ -0,0 +1,11 @@ +
      • +
        + + +
        + +
        + + +
        +
      • diff --git a/cms/static/client_templates/course_info_handouts.html b/cms/templates/js/course_info_handouts.underscore similarity index 100% rename from cms/static/client_templates/course_info_handouts.html rename to cms/templates/js/course_info_handouts.underscore diff --git a/cms/static/client_templates/course_info_update.html b/cms/templates/js/course_info_update.underscore similarity index 100% rename from cms/static/client_templates/course_info_update.html rename to cms/templates/js/course_info_update.underscore diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 22d57be41d..0ce0067da3 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -1,14 +1,16 @@ <%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%! from auth.authz import is_user_in_course_group_role %> <%inherit file="base.html" /> <%block name="title">${_("Course Team Settings")} -<%block name="bodyclass">is-signedin course users settings team +<%block name="bodyclass">is-signedin course users team <%block name="content">

        - ${_("Course Settings")} + ${_("Settings")} > ${_("Course Team")}

        @@ -17,7 +19,7 @@ @@ -25,111 +27,291 @@
        -
        -
        - -
        -

        ${_("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.")}

        -
        - -
        +
        +
        +
        %if allow_actions: -
        -
        -
        - - - -
        -
        - %endif -
        -
          - % for user in staff: -
        1. - ${user.username} - ${user.email} - %if allow_actions : -
          - %if request_user_id != user.id: - - %endif -
          - %endif -
        2. - % endfor -
        +
        +
        +
        +

        ${_("Add a User to Your Course's Team")}

        + +
        + ${_("New Team Member Information")} + +
          +
        1. + + + ${_("Please provide the email address of the course staff member you'd like to add")} +
        2. +
        +
        +
        + +
        + + +
        +
        + %endif + +
          + % for user in staff: + <% api_url = reverse('course_team_user', kwargs=dict( + org=context_course.location.org, + course=context_course.location.course, + name=context_course.location.name, + email=user.email, + )) + %> +
        1. + + <% is_instuctor = is_user_in_course_group_role(user, context_course.location, 'instructor', check_staff=False) %> + % if is_instuctor: + + + ${_("Current Role:")} + + ${_("Admin")} + % if request.user.id == user.id: + ${_("You!")} + % endif + + + + % else: + + + ${_("Current Role:")} + + ${_("Staff")} + % if request.user.id == user.id: + ${_("You!")} + % endif + + + + % endif + + + + % if allow_actions: + + % endif + +
        2. + % endfor +
        + + <% user_is_instuctor = is_user_in_course_group_role(request.user, context_course.location, 'instructor', check_staff=False) %> + % if user_is_instuctor and len(staff) == 1: +
        +
        +

        ${_('Add Team Members to This Course')}

        +
        +

        ${_('Adding team members makes course authoring collaborative. Users must be signed up for Studio and have an active account. ')}

        +
        +
        + + +
        + %endif
        -
        + + +
        <%block name="jsextra"> diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 1dde9b6c0d..1f5d89b2b9 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -262,7 +262,7 @@ from contentstore import utils diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index e1b1913c87..d87f3586b6 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -1,15 +1,17 @@ -<%! from django.utils.translation import ugettext as _ %> <%inherit file="base.html" /> +<%namespace name='static' file='static_content.html'/> <%! from django.core.urlresolvers import reverse %> +<%! from django.utils.translation import ugettext as _ %> +<%! from contentstore import utils %> <%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"> +% for template_name in ["advanced_entry"]: + +% endfor @@ -96,7 +98,7 @@ editor.render(); % endif diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index d9040009cc..f3a4584a26 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -140,7 +140,7 @@ from contentstore import utils diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index f001611b67..6178058689 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -60,7 +60,7 @@ ${_("Grading")}
        """ -DEFAULT_CONFIGURATION=""" + +DEFAULT_CONFIGURATION = """ Math.sqrt(r * r - x * x) -Math.sqrt(r * r - x * x) + Math.sqrt(r * r / 20 - Math.pow(x-r/2.5, 2)) + r/8 + -Math.sqrt(r * r / 20 - Math.pow(x-r/2.5, 2)) + r/5.5 + Math.sqrt(r * r / 20 - Math.pow(x+r/2.5, 2)) + r/8 + -Math.sqrt(r * r / 20 - Math.pow(x+r/2.5, 2)) + r/5.5 + -Math.sqrt(r * r / 5 - x * x) - r/5.5 @@ -54,10 +59,13 @@ DEFAULT_CONFIGURATION=""" """ - class GraphicalSliderToolFields(object): - render = String(scope=Scope.content, default=DEFAULT_RENDER) - configuration = String(scope=Scope.content, default=DEFAULT_CONFIGURATION) + data = String( + help="Html contents to display for this module", + default='{}{}'.format( + DEFAULT_RENDER, DEFAULT_CONFIGURATION), + scope=Scope.content + ) class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule): @@ -65,40 +73,54 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule): ''' js = { - 'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')], - 'js': [ - # 3rd party libraries used by graphic slider tool. - # TODO - where to store them - outside xmodule? - resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/state.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/el_output.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/g_label_el_output.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') - - ] + 'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')], + 'js': [ + # 3rd party libraries used by graphic slider tool. + # TODO - where to store them - outside xmodule? + resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/state.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/el_output.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/g_label_el_output.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') + ] } + css = {'scss': [resource_string(__name__, 'css/gst/display.scss')]} js_module_name = "GraphicalSliderTool" + @property + def configuration(self): + return stringify_children( + html.fromstring(self.data).xpath('configuration')[0] + ) + + @property + def render(self): + return stringify_children( + html.fromstring(self.data).xpath('render')[0] + ) + def get_html(self): """ Renders parameters to template. """ # these 3 will be used in class methods self.html_id = self.location.html_id() self.html_class = self.location.category + self.configuration_json = self.build_configuration_json() params = { - 'gst_html': self.substitute_controls(self.render), - 'element_id': self.html_id, - 'element_class': self.html_class, - 'configuration_json': self.configuration_json - } + 'gst_html': self.substitute_controls(self.render), + 'element_id': self.html_id, + 'element_class': self.html_class, + 'configuration_json': self.configuration_json + } content = self.system.render_template( - 'graphical_slider_tool.html', params) + 'graphical_slider_tool.html', params + ) return content def substitute_controls(self, html_string): @@ -126,9 +148,10 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule): if plot_el: plot_el = plot_el[0] plot_el.getparent().replace(plot_el, html.fromstring( - plot_div.format(element_class=self.html_class, - element_id=self.html_id, - style=plot_el.get('style', "")))) + plot_div.format( + element_class=self.html_class, + element_id=self.html_id, + style=plot_el.get('style', "")))) # substitute sliders slider_div = '
        added for interface compatibility with xmltodict.parse # class added for javascript's part purposes - return json.dumps(xmltodict.parse('' + self.configuration + '')) + root = '{}'.format( + self.html_class, + self.configuration) + return json.dumps(xmltodict.parse(root)) -class GraphicalSliderToolDescriptor(GraphicalSliderToolFields, MakoModuleDescriptor, XmlDescriptor): +class GraphicalSliderToolDescriptor(GraphicalSliderToolFields, XMLEditingDescriptor, XmlDescriptor): module_class = GraphicalSliderToolModule @classmethod @@ -202,24 +229,14 @@ class GraphicalSliderToolDescriptor(GraphicalSliderToolFields, MakoModuleDescrip exactly one '{0}' tag".format(child)) # finished - def parse(k): - """Assumes that xml_object has child k""" - return stringify_children(xml_object.xpath(k)[0]) return { - 'render': parse('render'), - 'configuration': parse('configuration') - }, [] + 'data': stringify_children(xml_object) + }, [] def definition_to_xml(self, resource_fs): '''Return an xml element representing this definition.''' - xml_object = etree.Element('graphical_slider_tool') - - def add_child(k): - child_str = '<{tag}>{body}'.format(tag=k, body=getattr(self, k)) - child_node = etree.fromstring(child_str) - xml_object.append(child_node) - - for child in ['render', 'configuration']: - add_child(child) - + data = '<{tag}>{body}'.format( + tag='graphical_slider_tool', + body=self.data) + xml_object = etree.fromstring(data) return xml_object diff --git a/common/lib/xmodule/xmodule/modulestore/locator.py b/common/lib/xmodule/xmodule/modulestore/locator.py index 928bc9f133..591ef3115f 100644 --- a/common/lib/xmodule/xmodule/modulestore/locator.py +++ b/common/lib/xmodule/xmodule/modulestore/locator.py @@ -45,7 +45,7 @@ class Locator(object): def __repr__(self): ''' - repr(self) returns something like this: CourseLocator("edu.mit.eecs.6002x") + repr(self) returns something like this: CourseLocator("mit.eecs.6002x") ''' classname = self.__class__.__name__ if classname.find('.') != -1: @@ -54,13 +54,13 @@ class Locator(object): def __str__(self): ''' - str(self) returns something like this: "edu.mit.eecs.6002x" + str(self) returns something like this: "mit.eecs.6002x" ''' return unicode(self).encode('utf8') def __unicode__(self): ''' - unicode(self) returns something like this: "edu.mit.eecs.6002x" + unicode(self) returns something like this: "mit.eecs.6002x" ''' return self.url() @@ -89,15 +89,15 @@ class CourseLocator(Locator): """ Examples of valid CourseLocator specifications: CourseLocator(version_guid=ObjectId('519665f6223ebd6980884f2b')) - CourseLocator(course_id='edu.mit.eecs.6002x') - CourseLocator(course_id='edu.mit.eecs.6002x;published') - CourseLocator(course_id='edu.mit.eecs.6002x', revision='published') + CourseLocator(course_id='mit.eecs.6002x') + CourseLocator(course_id='mit.eecs.6002x;published') + CourseLocator(course_id='mit.eecs.6002x', branch='published') CourseLocator(url='edx://@519665f6223ebd6980884f2b') - CourseLocator(url='edx://edu.mit.eecs.6002x') - CourseLocator(url='edx://edu.mit.eecs.6002x;published') + CourseLocator(url='edx://mit.eecs.6002x') + CourseLocator(url='edx://mit.eecs.6002x;published') Should have at lease a specific course_id (id for the course as if it were a project w/ - versions) with optional 'revision' (must be 'draft', 'published', or None), + versions) with optional 'branch', or version_guid (which points to a specific version). Can contain both in which case the persistence layer may raise exceptions if the given version != the current such version of the course. @@ -106,7 +106,7 @@ class CourseLocator(Locator): # Default values version_guid = None course_id = None - revision = None + branch = None def __unicode__(self): """ @@ -114,8 +114,8 @@ class CourseLocator(Locator): """ if self.course_id: result = self.course_id - if self.revision: - result += ';' + self.revision + if self.branch: + result += ';' + self.branch return result elif self.version_guid: return '@' + str(self.version_guid) @@ -131,7 +131,7 @@ class CourseLocator(Locator): # -- unused args which are used via inspect # pylint: disable= W0613 - def validate_args(self, url, version_guid, course_id, revision): + def validate_args(self, url, version_guid, course_id, branch): """ Validate provided arguments. """ @@ -144,12 +144,12 @@ class CourseLocator(Locator): def is_fully_specified(self): """ - Returns True if either version_guid is specified, or course_id+revision + Returns True if either version_guid is specified, or course_id+branch are specified. This should always return True, since this should be validated in the constructor. """ return self.version_guid is not None \ - or (self.course_id is not None and self.revision is not None) + or (self.course_id is not None and self.branch is not None) def set_course_id(self, new): """ @@ -158,12 +158,12 @@ class CourseLocator(Locator): """ self.set_property('course_id', new) - def set_revision(self, new): + def set_branch(self, new): """ - Initialize revision to new value. - If revision has already been initialized to a different value, raise an exception. + Initialize branch to new value. + If branch has already been initialized to a different value, raise an exception. """ - self.set_property('revision', new) + self.set_property('branch', new) def set_version_guid(self, new): """ @@ -181,29 +181,29 @@ class CourseLocator(Locator): """ return CourseLocator(course_id=self.course_id, version_guid=self.version_guid, - revision=self.revision) + branch=self.branch) - def __init__(self, url=None, version_guid=None, course_id=None, revision=None): + def __init__(self, url=None, version_guid=None, course_id=None, branch=None): """ Construct a CourseLocator Caller may provide url (but no other parameters). Caller may provide version_guid (but no other parameters). - Caller may provide course_id (optionally provide revision). + Caller may provide course_id (optionally provide branch). Resulting CourseLocator will have either a version_guid property - or a course_id (with optional revision) property, or both. + or a course_id (with optional branch) property, or both. version_guid must be an instance of bson.objectid.ObjectId or None - url, course_id, and revision must be strings or None + url, course_id, and branch must be strings or None """ - self.validate_args(url, version_guid, course_id, revision) + self.validate_args(url, version_guid, course_id, branch) if url: self.init_from_url(url) if version_guid: self.init_from_version_guid(version_guid) - if course_id or revision: - self.init_from_course_id(course_id, revision) + if course_id or branch: + self.init_from_course_id(course_id, branch) assert self.version_guid or self.course_id, \ "Either version_guid or course_id should be set." @@ -223,7 +223,7 @@ class CourseLocator(Locator): def init_from_url(self, url): """ url must be a string beginning with 'edx://' and containing - either a valid version_guid or course_id (with optional revision) + either a valid version_guid or course_id (with optional branch) If a block ('#HW3') is present, it is ignored. """ if isinstance(url, Locator): @@ -237,7 +237,7 @@ class CourseLocator(Locator): self.set_version_guid(self.as_object_id(new_guid)) else: self.set_course_id(parse['id']) - self.set_revision(parse['revision']) + self.set_branch(parse['branch']) def init_from_version_guid(self, version_guid): """ @@ -251,14 +251,14 @@ class CourseLocator(Locator): '%s is not an instance of ObjectId' % version_guid self.set_version_guid(version_guid) - def init_from_course_id(self, course_id, explicit_revision=None): + def init_from_course_id(self, course_id, explicit_branch=None): """ - Course_id is a string like 'edu.mit.eecs.6002x' or 'edu.mit.eecs.6002x;published'. + Course_id is a string like 'mit.eecs.6002x' or 'mit.eecs.6002x;published'. Revision (optional) is a string like 'published'. - It may be provided explicitly (explicit_revision) or embedded into course_id. - If revision is part of course_id ("...;published"), parse it out separately. - If revision is provided both ways, that's ok as long as they are the same value. + It may be provided explicitly (explicit_branch) or embedded into course_id. + If branch is part of course_id ("...;published"), parse it out separately. + If branch is provided both ways, that's ok as long as they are the same value. If a block ('#HW3') is a part of course_id, it is ignored. @@ -272,11 +272,11 @@ class CourseLocator(Locator): parse = parse_course_id(course_id) assert parse, 'Could not parse "%s" as a course_id' % course_id self.set_course_id(parse['id']) - rev = parse['revision'] + rev = parse['branch'] if rev: - self.set_revision(rev) - if explicit_revision: - self.set_revision(explicit_revision) + self.set_branch(rev) + if explicit_branch: + self.set_branch(explicit_branch) def version(self): """ @@ -305,37 +305,37 @@ class BlockUsageLocator(CourseLocator): the defined element in the course. Courses can be a version of an offering, the current draft head, or the current production version. - Locators can contain both a version and a course_id w/ revision. The split mongo functions - may raise errors if these conflict w/ the current db state (i.e., the course's revision != + Locators can contain both a version and a course_id w/ branch. The split mongo functions + may raise errors if these conflict w/ the current db state (i.e., the course's branch != the version_guid) Locations can express as urls as well as dictionaries. They consist of course_identifier: course_guid | version_guid block : guid - revision : 'draft' | 'published' (optional) + branch : string """ # Default value usage_id = None def __init__(self, url=None, version_guid=None, course_id=None, - revision=None, usage_id=None): + branch=None, usage_id=None): """ Construct a BlockUsageLocator - Caller may provide url, version_guid, or course_id, and optionally provide revision. + Caller may provide url, version_guid, or course_id, and optionally provide branch. The usage_id may be specified, either explictly or as part of the url or course_id. If omitted, the locator is created but it has not yet been initialized. Resulting BlockUsageLocator will have a usage_id property. - It will have either a version_guid property or a course_id (with optional revision) property, or both. + It will have either a version_guid property or a course_id (with optional branch) property, or both. version_guid must be an instance of bson.objectid.ObjectId or None - url, course_id, revision, and usage_id must be strings or None + url, course_id, branch, and usage_id must be strings or None """ - self.validate_args(url, version_guid, course_id, revision) + self.validate_args(url, version_guid, course_id, branch) if url: self.init_block_ref_from_url(url) if course_id: @@ -346,7 +346,7 @@ class BlockUsageLocator(CourseLocator): url=url, version_guid=version_guid, course_id=course_id, - revision=revision) + branch=branch) def is_initialized(self): """ @@ -366,11 +366,11 @@ class BlockUsageLocator(CourseLocator): """ if self.course_id and self.version_guid: return BlockUsageLocator(version_guid=self.version_guid, - revision=self.revision, + branch=self.branch, usage_id=self.usage_id) else: return BlockUsageLocator(course_id=self.course_id, - revision=self.revision, + branch=self.branch, usage_id=self.usage_id) def set_usage_id(self, new): diff --git a/common/lib/xmodule/xmodule/modulestore/parsers.py b/common/lib/xmodule/xmodule/modulestore/parsers.py index 9aac3073ae..8e5b685cec 100644 --- a/common/lib/xmodule/xmodule/modulestore/parsers.py +++ b/common/lib/xmodule/xmodule/modulestore/parsers.py @@ -20,7 +20,7 @@ def parse_url(string): with key 'version_guid' and the value, If it can be parsed as a course_id, returns a dict - with keys 'id' and 'revision' (value of 'revision' may be None), + with keys 'id' and 'branch' (value of 'branch' may be None), """ match = URL_RE.match(string) @@ -69,14 +69,14 @@ def parse_guid(string): return None -COURSE_ID_RE = re.compile(r'^(?P(\w+)(\.\w+\w*)*)(;(?P\w+))?(#(?P\w+))?$', re.IGNORECASE) +COURSE_ID_RE = re.compile(r'^(?P(\w+)(\.\w+\w*)*)(;(?P\w+))?(#(?P\w+))?$', re.IGNORECASE) def parse_course_id(string): r""" A course_id has a main id component. - There may also be an optional revision (;published or ;draft). + There may also be an optional branch (;published or ;draft). There may also be an optional block (#HW3 or #Quiz2). Examples of valid course_ids: @@ -89,11 +89,11 @@ def parse_course_id(string): Syntax: - course_id = main_id [; revision] [# block] + course_id = main_id [; branch] [# block] main_id = name [. name]* - revision = name + branch = name block = name @@ -104,8 +104,8 @@ def parse_course_id(string): and the underscore. (see definition of \w in python regular expressions, at http://docs.python.org/dev/library/re.html) - If string is a course_id, returns a dict with keys 'id', 'revision', and 'block'. - Revision is optional: if missing returned_dict['revision'] is None. + If string is a course_id, returns a dict with keys 'id', 'branch', and 'block'. + Revision is optional: if missing returned_dict['branch'] is None. Block is optional: if missing returned_dict['block'] is None. Else returns None. """ diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py index c9ef04420b..8a9b35e4f1 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/caching_descriptor_system.py @@ -81,7 +81,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): version_guid=course_entry_override['_id'], usage_id=usage_id, course_id=course_entry_override.get('course_id'), - revision=course_entry_override.get('revision') + branch=course_entry_override.get('branch') ) kvs = SplitMongoKVS( diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py index 6dd6fb480f..c8ed57d027 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py @@ -186,8 +186,8 @@ class SplitMongoModuleStore(ModuleStoreBase): return the CourseDescriptor! It returns the actual db json from structures. - Semantics: if course_id and revision given, then it will get that revision. If - also give a version_guid, it will see if the current head of that revision == that guid. If not + Semantics: if course_id and branch given, then it will get that branch. If + also give a version_guid, it will see if the current head of that branch == that guid. If not it raises VersionConflictError (the version now differs from what it was when you got your reference) @@ -198,19 +198,19 @@ class SplitMongoModuleStore(ModuleStoreBase): if not course_locator.is_fully_specified(): raise InsufficientSpecificationError('Not fully specified: %s' % course_locator) - if course_locator.course_id is not None and course_locator.revision is not None: + if course_locator.course_id is not None and course_locator.branch is not None: # use the course_id index = self.course_index.find_one({'_id': course_locator.course_id}) if index is None: raise ItemNotFoundError(course_locator) - if course_locator.revision not in index['versions']: + if course_locator.branch not in index['versions']: raise ItemNotFoundError(course_locator) - version_guid = index['versions'][course_locator.revision] + version_guid = index['versions'][course_locator.branch] if course_locator.version_guid is not None and version_guid != course_locator.version_guid: # This may be a bit too touchy but it's hard to infer intent raise VersionConflictError(course_locator, CourseLocator(course_locator, version_guid=version_guid)) else: - # TODO should this raise an exception if revision was provided? + # TODO should this raise an exception if branch was provided? version_guid = course_locator.version_guid # cast string to ObjectId if necessary @@ -223,29 +223,29 @@ class SplitMongoModuleStore(ModuleStoreBase): if course_locator.course_id: entry['course_id'] = course_locator.course_id - entry['revision'] = course_locator.revision + entry['branch'] = course_locator.branch return entry - def get_courses(self, revision, qualifiers=None): + def get_courses(self, branch, qualifiers=None): ''' Returns a list of course descriptors matching any given qualifiers. qualifiers should be a dict of keywords matching the db fields or any legal query for mongo to use against the active_versions collection. - Note, this is to find the current head of the named revision type + Note, this is to find the current head of the named branch type (e.g., 'draft'). To get specific versions via guid use get_course. ''' if qualifiers is None: qualifiers = {} - qualifiers.update({"versions.{}".format(revision): {"$exists": True}}) + qualifiers.update({"versions.{}".format(branch): {"$exists": True}}) matching = self.course_index.find(qualifiers) # collect ids and then query for those version_guids = [] id_version_map = {} for course_entry in matching: - version_guid = course_entry['versions'][revision] + version_guid = course_entry['versions'][branch] version_guids.append(version_guid) id_version_map[version_guid] = course_entry['_id'] @@ -667,7 +667,7 @@ class SplitMongoModuleStore(ModuleStoreBase): # update the index entry if appropriate if index_entry is not None: - self._update_head(index_entry, course_or_parent_locator.revision, new_id) + self._update_head(index_entry, course_or_parent_locator.branch, new_id) course_parent = course_or_parent_locator.as_course_locator() else: course_parent = None @@ -786,7 +786,7 @@ class SplitMongoModuleStore(ModuleStoreBase): 'edited_on': datetime.datetime.utcnow(), 'versions': versions_dict} new_id = self.course_index.insert(index_entry) - return self.get_course(CourseLocator(course_id=new_id, revision=master_version)) + return self.get_course(CourseLocator(course_id=new_id, branch=master_version)) def update_item(self, descriptor, user_id, force=False): """ @@ -835,7 +835,7 @@ class SplitMongoModuleStore(ModuleStoreBase): # update the index entry if appropriate if index_entry is not None: - self._update_head(index_entry, descriptor.location.revision, new_id) + self._update_head(index_entry, descriptor.location.branch, new_id) # fetch and return the new item--fetching is unnecessary but a good qc step return self.get_item(BlockUsageLocator(descriptor.location, version_guid=new_id)) @@ -876,7 +876,7 @@ class SplitMongoModuleStore(ModuleStoreBase): # update the index entry if appropriate if index_entry is not None: - self._update_head(index_entry, xblock.location.revision, new_id) + self._update_head(index_entry, xblock.location.branch, new_id) # fetch and return the new item--fetching is unnecessary but a good qc step return self.get_item(BlockUsageLocator(xblock.location, version_guid=new_id)) @@ -1028,9 +1028,9 @@ class SplitMongoModuleStore(ModuleStoreBase): # update the index entry if appropriate if index_entry is not None: - self._update_head(index_entry, usage_locator.revision, new_id) + self._update_head(index_entry, usage_locator.branch, new_id) result.course_id = usage_locator.course_id - result.revision = usage_locator.revision + result.branch = usage_locator.branch return result @@ -1186,19 +1186,19 @@ class SplitMongoModuleStore(ModuleStoreBase): :param locator: """ - if locator.course_id is None or locator.revision is None: + if locator.course_id is None or locator.branch is None: return None else: index_entry = self.course_index.find_one({'_id': locator.course_id}) if (locator.version_guid is not None - and index_entry['versions'][locator.revision] != locator.version_guid + and index_entry['versions'][locator.branch] != locator.version_guid and not force): raise VersionConflictError( locator, CourseLocator( course_id=index_entry['_id'], - version_guid=index_entry['versions'][locator.revision], - revision=locator.revision)) + version_guid=index_entry['versions'][locator.branch], + branch=locator.branch)) else: return index_entry @@ -1227,9 +1227,9 @@ class SplitMongoModuleStore(ModuleStoreBase): return False - def _update_head(self, index_entry, revision, new_id): + def _update_head(self, index_entry, branch, new_id): """ - Update the active index for the given course's revision to point to new_id + Update the active index for the given course's branch to point to new_id :param index_entry: :param course_locator: @@ -1237,4 +1237,4 @@ class SplitMongoModuleStore(ModuleStoreBase): """ self.course_index.update( {"_id": index_entry["_id"]}, - {"$set": {"versions.{}".format(revision): new_id}}) + {"$set": {"versions.{}".format(branch): new_id}}) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_locators.py b/common/lib/xmodule/xmodule/modulestore/tests/test_locators.py index 2626b6692d..bb41131234 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_locators.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_locators.py @@ -4,12 +4,10 @@ Created on Mar 14, 2013 @author: dmitchell ''' from unittest import TestCase -from nose.plugins.skip import SkipTest from bson.objectid import ObjectId from xmodule.modulestore.locator import Locator, CourseLocator, BlockUsageLocator -from xmodule.modulestore.exceptions import InvalidLocationError, \ - InsufficientSpecificationError, OverSpecificationError +from xmodule.modulestore.exceptions import InsufficientSpecificationError, OverSpecificationError class LocatorTest(TestCase): @@ -21,30 +19,30 @@ class LocatorTest(TestCase): self.assertRaises( OverSpecificationError, CourseLocator, - url='edx://edu.mit.eecs.6002x', - course_id='edu.harvard.history', - revision='published', + url='edx://mit.eecs.6002x', + course_id='harvard.history', + branch='published', version_guid=ObjectId()) self.assertRaises( OverSpecificationError, CourseLocator, - url='edx://edu.mit.eecs.6002x', - course_id='edu.harvard.history', + url='edx://mit.eecs.6002x', + course_id='harvard.history', version_guid=ObjectId()) self.assertRaises( OverSpecificationError, CourseLocator, - url='edx://edu.mit.eecs.6002x;published', - revision='draft') + url='edx://mit.eecs.6002x;published', + branch='draft') self.assertRaises( OverSpecificationError, CourseLocator, - course_id='edu.mit.eecs.6002x;published', - revision='draft') + course_id='mit.eecs.6002x;published', + branch='draft') def test_course_constructor_underspecified(self): self.assertRaises(InsufficientSpecificationError, CourseLocator) - self.assertRaises(InsufficientSpecificationError, CourseLocator, revision='published') + self.assertRaises(InsufficientSpecificationError, CourseLocator, branch='published') def test_course_constructor_bad_version_guid(self): self.assertRaises(ValueError, CourseLocator, version_guid="012345") @@ -73,467 +71,128 @@ class LocatorTest(TestCase): """ Test all sorts of badly-formed course_ids (and urls with those course_ids) """ - for bad_id in ('edu.mit.', - ' edu.mit.eecs', - 'edu.mit.eecs ', - '@edu.mit.eecs', - '#edu.mit.eecs', - 'edu.mit.ee cs', - 'edu.mit.ee,cs', - 'edu.mit.ee/cs', - 'edu.mit.ee$cs', - 'edu.mit.ee&cs', - 'edu.mit.ee()cs', + for bad_id in ('mit.', + ' mit.eecs', + 'mit.eecs ', + '@mit.eecs', + '#mit.eecs', + 'mit.ee cs', + 'mit.ee,cs', + 'mit.ee/cs', + 'mit.ee$cs', + 'mit.ee&cs', + 'mit.ee()cs', ';this', - 'edu.mit.eecs;', - 'edu.mit.eecs;this;that', - 'edu.mit.eecs;this;', - 'edu.mit.eecs;this ', - 'edu.mit.eecs;th%is ', + 'mit.eecs;', + 'mit.eecs;this;that', + 'mit.eecs;this;', + 'mit.eecs;this ', + 'mit.eecs;th%is ', ): self.assertRaises(AssertionError, CourseLocator, course_id=bad_id) self.assertRaises(AssertionError, CourseLocator, url='edx://' + bad_id) def test_course_constructor_bad_url(self): for bad_url in ('edx://', - 'edx:/edu.mit.eecs', - 'http://edu.mit.eecs', - 'edu.mit.eecs', - 'edx//edu.mit.eecs'): + 'edx:/mit.eecs', + 'http://mit.eecs', + 'mit.eecs', + 'edx//mit.eecs'): self.assertRaises(AssertionError, CourseLocator, url=bad_url) def test_course_constructor_redundant_001(self): - testurn = 'edu.mit.eecs.6002x' + testurn = 'mit.eecs.6002x' testobj = CourseLocator(course_id=testurn, url='edx://' + testurn) self.check_course_locn_fields(testobj, 'course_id', course_id=testurn) def test_course_constructor_redundant_002(self): - testurn = 'edu.mit.eecs.6002x;published' - expected_urn = 'edu.mit.eecs.6002x' + testurn = 'mit.eecs.6002x;published' + expected_urn = 'mit.eecs.6002x' expected_rev = 'published' testobj = CourseLocator(course_id=testurn, url='edx://' + testurn) self.check_course_locn_fields(testobj, 'course_id', course_id=expected_urn, - revision=expected_rev) + branch=expected_rev) - def test_course_constructor_course_id_no_revision(self): - testurn = 'edu.mit.eecs.6002x' + def test_course_constructor_course_id_no_branch(self): + testurn = 'mit.eecs.6002x' testobj = CourseLocator(course_id=testurn) self.check_course_locn_fields(testobj, 'course_id', course_id=testurn) self.assertEqual(testobj.course_id, testurn) self.assertEqual(str(testobj), testurn) self.assertEqual(testobj.url(), 'edx://' + testurn) - def test_course_constructor_course_id_with_revision(self): - testurn = 'edu.mit.eecs.6002x;published' - expected_id = 'edu.mit.eecs.6002x' - expected_revision = 'published' + def test_course_constructor_course_id_with_branch(self): + testurn = 'mit.eecs.6002x;published' + expected_id = 'mit.eecs.6002x' + expected_branch = 'published' testobj = CourseLocator(course_id=testurn) - self.check_course_locn_fields(testobj, 'course_id with revision', + self.check_course_locn_fields(testobj, 'course_id with branch', course_id=expected_id, - revision=expected_revision, + branch=expected_branch, ) self.assertEqual(testobj.course_id, expected_id) - self.assertEqual(testobj.revision, expected_revision) + self.assertEqual(testobj.branch, expected_branch) self.assertEqual(str(testobj), testurn) self.assertEqual(testobj.url(), 'edx://' + testurn) - def test_course_constructor_course_id_separate_revision(self): - test_id = 'edu.mit.eecs.6002x' - test_revision = 'published' - expected_urn = 'edu.mit.eecs.6002x;published' - testobj = CourseLocator(course_id=test_id, revision=test_revision) - self.check_course_locn_fields(testobj, 'course_id with separate revision', + def test_course_constructor_course_id_separate_branch(self): + test_id = 'mit.eecs.6002x' + test_branch = 'published' + expected_urn = 'mit.eecs.6002x;published' + testobj = CourseLocator(course_id=test_id, branch=test_branch) + self.check_course_locn_fields(testobj, 'course_id with separate branch', course_id=test_id, - revision=test_revision, + branch=test_branch, ) self.assertEqual(testobj.course_id, test_id) - self.assertEqual(testobj.revision, test_revision) + self.assertEqual(testobj.branch, test_branch) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn) - def test_course_constructor_course_id_repeated_revision(self): + def test_course_constructor_course_id_repeated_branch(self): """ - The same revision appears in the course_id and the revision field. + The same branch appears in the course_id and the branch field. """ - test_id = 'edu.mit.eecs.6002x;published' - test_revision = 'published' - expected_id = 'edu.mit.eecs.6002x' - expected_urn = 'edu.mit.eecs.6002x;published' - testobj = CourseLocator(course_id=test_id, revision=test_revision) - self.check_course_locn_fields(testobj, 'course_id with repeated revision', + test_id = 'mit.eecs.6002x;published' + test_branch = 'published' + expected_id = 'mit.eecs.6002x' + expected_urn = 'mit.eecs.6002x;published' + testobj = CourseLocator(course_id=test_id, branch=test_branch) + self.check_course_locn_fields(testobj, 'course_id with repeated branch', course_id=expected_id, - revision=test_revision, + branch=test_branch, ) self.assertEqual(testobj.course_id, expected_id) - self.assertEqual(testobj.revision, test_revision) + self.assertEqual(testobj.branch, test_branch) self.assertEqual(str(testobj), expected_urn) self.assertEqual(testobj.url(), 'edx://' + expected_urn) def test_block_constructor(self): - testurn = 'edu.mit.eecs.6002x;published#HW3' - expected_id = 'edu.mit.eecs.6002x' - expected_revision = 'published' + testurn = 'mit.eecs.6002x;published#HW3' + expected_id = 'mit.eecs.6002x' + expected_branch = 'published' expected_block_ref = 'HW3' testobj = BlockUsageLocator(course_id=testurn) self.check_block_locn_fields(testobj, 'test_block constructor', course_id=expected_id, - revision=expected_revision, + branch=expected_branch, block=expected_block_ref) self.assertEqual(str(testobj), testurn) self.assertEqual(testobj.url(), 'edx://' + testurn) - # ------------------------------------------------------------ - # Disabled tests - - def test_course_urls(self): - ''' - Test constructor and property accessors. - ''' - raise SkipTest() - self.assertRaises(TypeError, CourseLocator, 'empty constructor') - - # url inits - testurn = 'edx://org/course/category/name' - self.assertRaises(InvalidLocationError, CourseLocator, url=testurn) - testurn = 'unknown/versionid/blockid' - self.assertRaises(InvalidLocationError, CourseLocator, url=testurn) - - testurn = 'cvx/versionid' - testobj = CourseLocator(testurn) - self.check_course_locn_fields(testobj, testurn, 'versionid') - self.assertEqual(testobj, CourseLocator(testobj), - 'initialization from another instance') - - testurn = 'cvx/versionid/' - testobj = CourseLocator(testurn) - self.check_course_locn_fields(testobj, testurn, 'versionid') - - testurn = 'cvx/versionid/blockid' - testobj = CourseLocator(testurn) - self.check_course_locn_fields(testobj, testurn, 'versionid') - - testurn = 'cvx/versionid/blockid/extraneousstuff?including=args' - testobj = CourseLocator(testurn) - self.check_course_locn_fields(testobj, testurn, 'versionid') - - testurn = 'cvx://versionid/blockid' - testobj = CourseLocator(testurn) - self.check_course_locn_fields(testobj, testurn, 'versionid') - - testurn = 'crx/courseid/blockid' - testobj = CourseLocator(testurn) - self.check_course_locn_fields(testobj, testurn, course_id='courseid') - - testurn = 'crx/courseid@revision/blockid' - testobj = CourseLocator(testurn) - self.check_course_locn_fields(testobj, testurn, course_id='courseid', - revision='revision') - self.assertEqual(testobj, CourseLocator(testobj), - 'run initialization from another instance') - - def test_course_keyword_setters(self): - raise SkipTest() - # arg list inits - testobj = CourseLocator(version_guid='versionid') - self.check_course_locn_fields(testobj, 'versionid arg', 'versionid') - - testobj = CourseLocator(course_id='courseid') - self.check_course_locn_fields(testobj, 'courseid arg', - course_id='courseid') - - testobj = CourseLocator(course_id='courseid', revision='rev') - self.check_course_locn_fields(testobj, 'rev arg', - course_id='courseid', - revision='rev') - # ignores garbage - testobj = CourseLocator(course_id='courseid', revision='rev', - potato='spud') - self.check_course_locn_fields(testobj, 'extra keyword arg', - course_id='courseid', - revision='rev') - - # url w/ keyword override - testurn = 'crx/courseid@revision/blockid' - testobj = CourseLocator(testurn, revision='rev') - self.check_course_locn_fields(testobj, 'rev override', - course_id='courseid', - revision='rev') - - def test_course_dict(self): - raise SkipTest() - # dict init w/ keyword overwrites - testobj = CourseLocator({"version_guid": 'versionid'}) - self.check_course_locn_fields(testobj, 'versionid dict', 'versionid') - - testobj = CourseLocator({"course_id": 'courseid'}) - self.check_course_locn_fields(testobj, 'courseid dict', - course_id='courseid') - - testobj = CourseLocator({"course_id": 'courseid', "revision": 'rev'}) - self.check_course_locn_fields(testobj, 'rev dict', - course_id='courseid', - revision='rev') - # ignores garbage - testobj = CourseLocator({"course_id": 'courseid', "revision": 'rev', - "potato": 'spud'}) - self.check_course_locn_fields(testobj, 'extra keyword dict', - course_id='courseid', - revision='rev') - testobj = CourseLocator({"course_id": 'courseid', "revision": 'rev'}, - revision='alt') - self.check_course_locn_fields(testobj, 'rev dict', - course_id='courseid', - revision='alt') - - # urn init w/ dict & keyword overwrites - testobj = CourseLocator('crx/notcourse@notthis', - {"course_id": 'courseid'}, - revision='alt') - self.check_course_locn_fields(testobj, 'rev dict', - course_id='courseid', - revision='alt') - - def test_url(self): - ''' - Ensure CourseLocator generates expected urls. - ''' - raise SkipTest() - - testobj = CourseLocator(version_guid='versionid') - self.assertEqual(testobj.url(), 'cvx/versionid', 'versionid') - self.assertEqual(testobj, CourseLocator(testobj.url()), - 'versionid conversion through url') - - testobj = CourseLocator(course_id='courseid') - self.assertEqual(testobj.url(), 'crx/courseid', 'courseid') - self.assertEqual(testobj, CourseLocator(testobj.url()), - 'courseid conversion through url') - - testobj = CourseLocator(course_id='courseid', revision='rev') - self.assertEqual(testobj.url(), 'crx/courseid@rev', 'rev') - self.assertEqual(testobj, CourseLocator(testobj.url()), - 'rev conversion through url') - - def test_html(self): - ''' - Ensure CourseLocator generates expected urls. - ''' - raise SkipTest() - testobj = CourseLocator(version_guid='versionid') - self.assertEqual(testobj.html_id(), 'cvx/versionid', 'versionid') - self.assertEqual(testobj, CourseLocator(testobj.html_id()), - 'versionid conversion through html_id') - - testobj = CourseLocator(course_id='courseid') - self.assertEqual(testobj.html_id(), 'crx/courseid', 'courseid') - self.assertEqual(testobj, CourseLocator(testobj.html_id()), - 'courseid conversion through html_id') - - testobj = CourseLocator(course_id='courseid', revision='rev') - self.assertEqual(testobj.html_id(), 'crx/courseid%40rev', 'rev') - self.assertEqual(testobj, CourseLocator(testobj.html_id()), - 'rev conversion through html_id') - - def test_block_locator(self): - ''' - Test constructor and property accessors. - ''' - raise SkipTest() - self.assertIsInstance(BlockUsageLocator(), BlockUsageLocator, - 'empty constructor') - - # url inits - testurn = 'edx://org/course/category/name' - self.assertRaises(InvalidLocationError, BlockUsageLocator, testurn) - testurn = 'unknown/versionid/blockid' - self.assertRaises(InvalidLocationError, BlockUsageLocator, testurn) - - testurn = 'cvx/versionid' - testobj = BlockUsageLocator(testurn) - self.check_block_locn_fields(testobj, testurn, 'versionid') - self.assertEqual(testobj, BlockUsageLocator(testobj), - 'initialization from another instance') - - testurn = 'cvx/versionid/' - testobj = BlockUsageLocator(testurn) - self.check_block_locn_fields(testobj, testurn, 'versionid') - - testurn = 'cvx/versionid/blockid' - testobj = BlockUsageLocator(testurn) - self.check_block_locn_fields(testobj, testurn, 'versionid', - block='blockid') - - testurn = 'cvx/versionid/blockid/extraneousstuff?including=args' - testobj = BlockUsageLocator(testurn) - self.check_block_locn_fields(testobj, testurn, 'versionid', - block='blockid') - - testurn = 'cvx://versionid/blockid' - testobj = BlockUsageLocator(testurn) - self.check_block_locn_fields(testobj, testurn, 'versionid', - block='blockid') - - testurn = 'crx/courseid/blockid' - testobj = BlockUsageLocator(testurn) - self.check_block_locn_fields(testobj, testurn, course_id='courseid', - block='blockid') - - testurn = 'crx/courseid@revision/blockid' - testobj = BlockUsageLocator(testurn) - self.check_block_locn_fields(testobj, testurn, course_id='courseid', - revision='revision', block='blockid') - self.assertEqual(testobj, BlockUsageLocator(testobj), - 'run initialization from another instance') - - def test_block_keyword_init(self): - # arg list inits - raise SkipTest() - testobj = BlockUsageLocator(version_guid='versionid') - self.check_block_locn_fields(testobj, 'versionid arg', 'versionid') - - testobj = BlockUsageLocator(version_guid='versionid', usage_id='myblock') - self.check_block_locn_fields(testobj, 'versionid arg', 'versionid', - block='myblock') - - testobj = BlockUsageLocator(course_id='courseid') - self.check_block_locn_fields(testobj, 'courseid arg', - course_id='courseid') - - testobj = BlockUsageLocator(course_id='courseid', revision='rev') - self.check_block_locn_fields(testobj, 'rev arg', - course_id='courseid', - revision='rev') - # ignores garbage - testobj = BlockUsageLocator(course_id='courseid', revision='rev', - usage_id='this_block', potato='spud') - self.check_block_locn_fields(testobj, 'extra keyword arg', - course_id='courseid', block='this_block', revision='rev') - - # url w/ keyword override - testurn = 'crx/courseid@revision/blockid' - testobj = BlockUsageLocator(testurn, revision='rev') - self.check_block_locn_fields(testobj, 'rev override', - course_id='courseid', block='blockid', - revision='rev') - - def test_block_keywords(self): - # dict init w/ keyword overwrites - raise SkipTest() - testobj = BlockUsageLocator({"version_guid": 'versionid', - 'usage_id': 'dictblock'}) - self.check_block_locn_fields(testobj, 'versionid dict', 'versionid', - block='dictblock') - - testobj = BlockUsageLocator({"course_id": 'courseid', - 'usage_id': 'dictblock'}) - self.check_block_locn_fields(testobj, 'courseid dict', - block='dictblock', course_id='courseid') - - testobj = BlockUsageLocator({"course_id": 'courseid', "revision": 'rev', - 'usage_id': 'dictblock'}) - self.check_block_locn_fields(testobj, 'rev dict', - course_id='courseid', block='dictblock', - revision='rev') - # ignores garbage - testobj = BlockUsageLocator({"course_id": 'courseid', "revision": 'rev', - 'usage_id': 'dictblock', "potato": 'spud'}) - self.check_block_locn_fields(testobj, 'extra keyword dict', - course_id='courseid', block='dictblock', - revision='rev') - testobj = BlockUsageLocator({"course_id": 'courseid', "revision": 'rev', - 'usage_id': 'dictblock'}, revision='alt', usage_id='anotherblock') - self.check_block_locn_fields(testobj, 'rev dict', - course_id='courseid', block='anotherblock', - revision='alt') - - # urn init w/ dict & keyword overwrites - testobj = BlockUsageLocator('crx/notcourse@notthis/northis', - {"course_id": 'courseid'}, revision='alt', usage_id='anotherblock') - self.check_block_locn_fields(testobj, 'rev dict', - course_id='courseid', block='anotherblock', - revision='alt') - - def test_ensure_fully_specd(self): - ''' - Test constructor and property accessors. - ''' - raise SkipTest() - self.assertRaises(InsufficientSpecificationError, - BlockUsageLocator.ensure_fully_specified, BlockUsageLocator()) - - # url inits - testurn = 'edx://org/course/category/name' - self.assertRaises(InvalidLocationError, - BlockUsageLocator.ensure_fully_specified, testurn) - testurn = 'unknown/versionid/blockid' - self.assertRaises(InvalidLocationError, - BlockUsageLocator.ensure_fully_specified, testurn) - - testurn = 'cvx/versionid' - self.assertRaises(InsufficientSpecificationError, - BlockUsageLocator.ensure_fully_specified, testurn) - - testurn = 'cvx/versionid/' - self.assertRaises(InsufficientSpecificationError, - BlockUsageLocator.ensure_fully_specified, testurn) - - testurn = 'cvx/versionid/blockid' - self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn), - BlockUsageLocator, testurn) - - testurn = 'cvx/versionid/blockid/extraneousstuff?including=args' - self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn), - BlockUsageLocator, testurn) - - testurn = 'cvx://versionid/blockid' - self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn), - BlockUsageLocator, testurn) - - testurn = 'crx/courseid/blockid' - self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn), - BlockUsageLocator, testurn) - - testurn = 'crx/courseid@revision/blockid' - self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn), - BlockUsageLocator, testurn) - - def test_ensure_fully_via_keyword(self): - # arg list inits - raise SkipTest() - testobj = BlockUsageLocator(version_guid='versionid') - self.assertRaises(InsufficientSpecificationError, - BlockUsageLocator.ensure_fully_specified, testobj) - - testurn = 'crx/courseid@revision/blockid' - testobj = BlockUsageLocator(version_guid='versionid', usage_id='myblock') - self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn), - BlockUsageLocator, testurn) - - testobj = BlockUsageLocator(course_id='courseid') - self.assertRaises(InsufficientSpecificationError, - BlockUsageLocator.ensure_fully_specified, testobj) - - testobj = BlockUsageLocator(course_id='courseid', revision='rev') - self.assertRaises(InsufficientSpecificationError, - BlockUsageLocator.ensure_fully_specified, testobj) - - testobj = BlockUsageLocator(course_id='courseid', revision='rev', - usage_id='this_block') - self.assertIsInstance(BlockUsageLocator.ensure_fully_specified(testurn), - BlockUsageLocator, testurn) # ------------------------------------------------------------------ # Utilities def check_course_locn_fields(self, testobj, msg, version_guid=None, - course_id=None, revision=None): + course_id=None, branch=None): self.assertEqual(testobj.version_guid, version_guid, msg) self.assertEqual(testobj.course_id, course_id, msg) - self.assertEqual(testobj.revision, revision, msg) + self.assertEqual(testobj.branch, branch, msg) def check_block_locn_fields(self, testobj, msg, version_guid=None, - course_id=None, revision=None, block=None): + course_id=None, branch=None, block=None): self.check_course_locn_fields(testobj, msg, version_guid, course_id, - revision) + branch) self.assertEqual(testobj.usage_id, block) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py index 29f6cce919..d71223f59b 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py @@ -69,10 +69,17 @@ class SplitModuleTest(unittest.TestCase): collection_prefix + collection, '--jsonArray', '--file', SplitModuleTest.COMMON_ROOT + '/test/data/splitmongo_json/' + collection + '.json' - ]) + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) for collection in ('active_versions', 'structures', 'definitions')] for p in processes: - if p.wait() != 0: + stdout, stderr = p.communicate() + if p.returncode != 0: + print "Couldn't run mongoimport:" + print stdout + print stderr raise Exception("DB did not init correctly") @classmethod @@ -129,8 +136,8 @@ class SplitModuleCourseTests(SplitModuleTest): self.assertEqual(str(course.previous_version), self.GUID_D1) self.assertDictEqual(course.grade_cutoffs, {"Pass": 0.45}) - def test_revision_requests(self): - # query w/ revision qualifier (both draft and published) + def test_branch_requests(self): + # query w/ branch qualifier (both draft and published) courses_published = modulestore().get_courses('published') self.assertEqual(len(courses_published), 1, len(courses_published)) course = self.findByIdInResult(courses_published, "head23456") @@ -182,7 +189,7 @@ class SplitModuleCourseTests(SplitModuleTest): self.assertEqual(course.edited_by, "testassist@edx.org") self.assertDictEqual(course.grade_cutoffs, {"Pass": 0.55}) - locator = CourseLocator(course_id='GreekHero', revision='draft') + locator = CourseLocator(course_id='GreekHero', branch='draft') course = modulestore().get_course(locator) self.assertEqual(course.location.course_id, "GreekHero") self.assertEqual(str(course.location.version_guid), self.GUID_D0) @@ -195,12 +202,12 @@ class SplitModuleCourseTests(SplitModuleTest): self.assertEqual(course.edited_by, "testassist@edx.org") self.assertDictEqual(course.grade_cutoffs, {"Pass": 0.45}) - locator = CourseLocator(course_id='wonderful', revision='published') + locator = CourseLocator(course_id='wonderful', branch='published') course = modulestore().get_course(locator) self.assertEqual(course.location.course_id, "wonderful") self.assertEqual(str(course.location.version_guid), self.GUID_P) - locator = CourseLocator(course_id='wonderful', revision='draft') + locator = CourseLocator(course_id='wonderful', branch='draft') course = modulestore().get_course(locator) self.assertEqual(str(course.location.version_guid), self.GUID_D2) @@ -209,10 +216,10 @@ class SplitModuleCourseTests(SplitModuleTest): self.assertRaises(InsufficientSpecificationError, modulestore().get_course, CourseLocator(course_id='edu.meh.blah')) self.assertRaises(ItemNotFoundError, - modulestore().get_course, CourseLocator(course_id='nosuchthing', revision='draft')) + modulestore().get_course, CourseLocator(course_id='nosuchthing', branch='draft')) self.assertRaises(ItemNotFoundError, modulestore().get_course, - CourseLocator(course_id='GreekHero', revision='published')) + CourseLocator(course_id='GreekHero', branch='published')) def test_course_successors(self): """ @@ -250,7 +257,7 @@ class SplitModuleItemTests(SplitModuleTest): self.assertTrue(modulestore().has_item(locator), "couldn't find in %s" % self.GUID_D1) - locator = BlockUsageLocator(course_id='GreekHero', usage_id='head12345', revision='draft') + locator = BlockUsageLocator(course_id='GreekHero', usage_id='head12345', branch='draft') self.assertTrue( modulestore().has_item(locator), "couldn't find in 12345" @@ -258,7 +265,7 @@ class SplitModuleItemTests(SplitModuleTest): self.assertTrue( modulestore().has_item(BlockUsageLocator( course_id=locator.course_id, - revision='draft', + branch='draft', usage_id=locator.usage_id )), "couldn't find in draft 12345" @@ -266,38 +273,38 @@ class SplitModuleItemTests(SplitModuleTest): self.assertFalse( modulestore().has_item(BlockUsageLocator( course_id=locator.course_id, - revision='published', + branch='published', usage_id=locator.usage_id)), "found in published 12345" ) - locator.revision = 'draft' + locator.branch = 'draft' self.assertTrue( modulestore().has_item(locator), "not found in draft 12345" ) # not a course obj - locator = BlockUsageLocator(course_id='GreekHero', usage_id='chapter1', revision='draft') + locator = BlockUsageLocator(course_id='GreekHero', usage_id='chapter1', branch='draft') self.assertTrue( modulestore().has_item(locator), "couldn't find chapter1" ) # in published course - locator = BlockUsageLocator(course_id="wonderful", usage_id="head23456", revision='draft') + locator = BlockUsageLocator(course_id="wonderful", usage_id="head23456", branch='draft') self.assertTrue(modulestore().has_item(BlockUsageLocator(course_id=locator.course_id, usage_id=locator.usage_id, - revision='published')), + branch='published')), "couldn't find in 23456") - locator.revision = 'published' + locator.branch = 'published' self.assertTrue(modulestore().has_item(locator), "couldn't find in 23456") def test_negative_has_item(self): # negative tests--not found # no such course or block - locator = BlockUsageLocator(course_id="doesnotexist", usage_id="head23456", revision='draft') + locator = BlockUsageLocator(course_id="doesnotexist", usage_id="head23456", branch='draft') self.assertFalse(modulestore().has_item(locator)) - locator = BlockUsageLocator(course_id="wonderful", usage_id="doesnotexist", revision='draft') + locator = BlockUsageLocator(course_id="wonderful", usage_id="doesnotexist", branch='draft') self.assertFalse(modulestore().has_item(locator)) # negative tests--insufficient specification @@ -316,7 +323,7 @@ class SplitModuleItemTests(SplitModuleTest): block = modulestore().get_item(locator) self.assertIsInstance(block, CourseDescriptor) - locator = BlockUsageLocator(course_id='GreekHero', usage_id='head12345', revision='draft') + locator = BlockUsageLocator(course_id='GreekHero', usage_id='head12345', branch='draft') block = modulestore().get_item(locator) self.assertEqual(block.location.course_id, "GreekHero") # look at this one in detail @@ -331,13 +338,13 @@ class SplitModuleItemTests(SplitModuleTest): block.grade_cutoffs, {"Pass": 0.45}, ) - # try to look up other revisions + # try to look up other branches self.assertRaises(ItemNotFoundError, modulestore().get_item, BlockUsageLocator(course_id=locator.as_course_locator(), usage_id=locator.usage_id, - revision='published')) - locator.revision = 'draft' + branch='published')) + locator.branch = 'draft' self.assertIsInstance( modulestore().get_item(locator), CourseDescriptor @@ -345,7 +352,7 @@ class SplitModuleItemTests(SplitModuleTest): def test_get_non_root(self): # not a course obj - locator = BlockUsageLocator(course_id='GreekHero', usage_id='chapter1', revision='draft') + locator = BlockUsageLocator(course_id='GreekHero', usage_id='chapter1', branch='draft') block = modulestore().get_item(locator) self.assertEqual(block.location.course_id, "GreekHero") self.assertEqual(block.category, 'chapter') @@ -354,7 +361,7 @@ class SplitModuleItemTests(SplitModuleTest): self.assertEqual(block.edited_by, "testassist@edx.org") # in published course - locator = BlockUsageLocator(course_id="wonderful", usage_id="head23456", revision='published') + locator = BlockUsageLocator(course_id="wonderful", usage_id="head23456", branch='published') self.assertIsInstance( modulestore().get_item(locator), CourseDescriptor @@ -362,10 +369,10 @@ class SplitModuleItemTests(SplitModuleTest): # negative tests--not found # no such course or block - locator = BlockUsageLocator(course_id="doesnotexist", usage_id="head23456", revision='draft') + locator = BlockUsageLocator(course_id="doesnotexist", usage_id="head23456", branch='draft') with self.assertRaises(ItemNotFoundError): modulestore().get_item(locator) - locator = BlockUsageLocator(course_id="wonderful", usage_id="doesnotexist", revision='draft') + locator = BlockUsageLocator(course_id="wonderful", usage_id="doesnotexist", branch='draft') with self.assertRaises(ItemNotFoundError): modulestore().get_item(locator) @@ -373,7 +380,7 @@ class SplitModuleItemTests(SplitModuleTest): with self.assertRaises(InsufficientSpecificationError): modulestore().get_item(BlockUsageLocator(version_guid=self.GUID_D1)) with self.assertRaises(InsufficientSpecificationError): - modulestore().get_item(BlockUsageLocator(course_id='GreekHero', revision='draft')) + modulestore().get_item(BlockUsageLocator(course_id='GreekHero', branch='draft')) # pylint: disable=W0212 def test_matching(self): @@ -404,7 +411,7 @@ class SplitModuleItemTests(SplitModuleTest): def test_get_items(self): ''' - get_items(locator, qualifiers, [revision]) + get_items(locator, qualifiers, [branch]) ''' locator = CourseLocator(version_guid=self.GUID_D0) # get all modules @@ -429,9 +436,9 @@ class SplitModuleItemTests(SplitModuleTest): def test_get_parents(self): ''' - get_parent_locations(locator, [usage_id], [revision]): [BlockUsageLocator] + get_parent_locations(locator, [usage_id], [branch]): [BlockUsageLocator] ''' - locator = CourseLocator(course_id="GreekHero", revision='draft') + locator = CourseLocator(course_id="GreekHero", branch='draft') parents = modulestore().get_parent_locations(locator, usage_id='chapter1') self.assertEqual(len(parents), 1) self.assertEqual(parents[0].usage_id, 'head12345') @@ -447,7 +454,7 @@ class SplitModuleItemTests(SplitModuleTest): """ Test the existing get_children method on xdescriptors """ - locator = BlockUsageLocator(course_id="GreekHero", usage_id="head12345", revision='draft') + locator = BlockUsageLocator(course_id="GreekHero", usage_id="head12345", branch='draft') block = modulestore().get_item(locator) children = block.get_children() expected_ids = [ @@ -490,7 +497,7 @@ class TestItemCrud(SplitModuleTest): metadata=None): new_desciptor """ # grab link to course to ensure new versioning works - locator = CourseLocator(course_id="GreekHero", revision='draft') + locator = CourseLocator(course_id="GreekHero", branch='draft') premod_course = modulestore().get_course(locator) premod_time = datetime.datetime.now(UTC) - datetime.timedelta(seconds=1) # add minimal one w/o a parent @@ -527,7 +534,7 @@ class TestItemCrud(SplitModuleTest): """ Test create_item w/ specifying the parent of the new item """ - locator = BlockUsageLocator(course_id="wonderful", usage_id="head23456", revision='draft') + locator = BlockUsageLocator(course_id="wonderful", usage_id="head23456", branch='draft') premod_course = modulestore().get_course(locator) category = 'chapter' new_module = modulestore().create_item( @@ -547,7 +554,7 @@ class TestItemCrud(SplitModuleTest): a definition id and new def data that it branches the definition in the db. Actually, this tries to test all create_item features not tested above. """ - locator = BlockUsageLocator(course_id="contender", usage_id="head345679", revision='draft') + locator = BlockUsageLocator(course_id="contender", usage_id="head345679", branch='draft') category = 'problem' premod_time = datetime.datetime.now(UTC) - datetime.timedelta(seconds=1) new_payload = "empty" @@ -585,7 +592,7 @@ class TestItemCrud(SplitModuleTest): """ test updating an items metadata ensuring the definition doesn't version but the course does if it should """ - locator = BlockUsageLocator(course_id="GreekHero", usage_id="problem3_2", revision='draft') + locator = BlockUsageLocator(course_id="GreekHero", usage_id="problem3_2", branch='draft') problem = modulestore().get_item(locator) pre_def_id = problem.definition_locator.definition_id pre_version_guid = problem.location.version_guid @@ -622,7 +629,7 @@ class TestItemCrud(SplitModuleTest): """ test updating an item's children ensuring the definition doesn't version but the course does if it should """ - locator = BlockUsageLocator(course_id="GreekHero", usage_id="chapter3", revision='draft') + locator = BlockUsageLocator(course_id="GreekHero", usage_id="chapter3", branch='draft') block = modulestore().get_item(locator) pre_def_id = block.definition_locator.definition_id pre_version_guid = block.location.version_guid @@ -646,7 +653,7 @@ class TestItemCrud(SplitModuleTest): """ test updating an item's definition: ensure it gets versioned as well as the course getting versioned """ - locator = BlockUsageLocator(course_id="GreekHero", usage_id="head12345", revision='draft') + locator = BlockUsageLocator(course_id="GreekHero", usage_id="head12345", branch='draft') block = modulestore().get_item(locator) pre_def_id = block.definition_locator.definition_id pre_version_guid = block.location.version_guid @@ -663,7 +670,7 @@ class TestItemCrud(SplitModuleTest): Test updating metadata, children, and definition in a single call ensuring all the versioning occurs """ # first add 2 children to the course for the update to manipulate - locator = BlockUsageLocator(course_id="contender", usage_id="head345679", revision='draft') + locator = BlockUsageLocator(course_id="contender", usage_id="head345679", branch='draft') category = 'problem' new_payload = "empty" modulestore().create_item( @@ -707,14 +714,14 @@ class TestItemCrud(SplitModuleTest): reusable_location = BlockUsageLocator( course_id=course.location.course_id, usage_id=course.location.usage_id, - revision='draft') + branch='draft') # delete a leaf problems = modulestore().get_items(reusable_location, {'category': 'problem'}) locn_to_del = problems[0].location new_course_loc = modulestore().delete_item(locn_to_del, 'deleting_user') deleted = BlockUsageLocator(course_id=reusable_location.course_id, - revision=reusable_location.revision, + branch=reusable_location.branch, usage_id=locn_to_del.usage_id) self.assertFalse(modulestore().has_item(deleted)) self.assertRaises(VersionConflictError, modulestore().has_item, locn_to_del) @@ -736,7 +743,7 @@ class TestItemCrud(SplitModuleTest): self.assertFalse(modulestore().has_item( BlockUsageLocator( course_id=node_loc.course_id, - revision=node_loc.revision, + branch=node_loc.branch, usage_id=node.location.usage_id))) locator = BlockUsageLocator( version_guid=node.location.version_guid, @@ -752,7 +759,7 @@ class TestItemCrud(SplitModuleTest): root = BlockUsageLocator( course_id=course.location.course_id, usage_id=course.location.usage_id, - revision='draft') + branch='draft') for _ in range(4): self.create_subtree_for_deletion(root, ['chapter', 'vertical', 'problem']) return modulestore().get_item(root) @@ -807,7 +814,7 @@ class TestCourseCreation(SplitModuleTest): Test making a course which points to an existing draft and published but not making any changes to either. """ pre_time = datetime.datetime.now(UTC) - original_locator = CourseLocator(course_id="wonderful", revision='draft') + original_locator = CourseLocator(course_id="wonderful", branch='draft') original_index = modulestore().get_course_index_info(original_locator) new_draft = modulestore().create_course( 'leech', 'best_course', 'leech_master', id_root='best', @@ -824,7 +831,7 @@ class TestCourseCreation(SplitModuleTest): self.assertLessEqual(new_index["edited_on"], datetime.datetime.now(UTC)) self.assertEqual(new_index['edited_by'], 'leech_master') - new_published_locator = CourseLocator(course_id=new_draft_locator.course_id, revision='published') + new_published_locator = CourseLocator(course_id=new_draft_locator.course_id, branch='published') new_published = modulestore().get_course(new_published_locator) self.assertEqual(new_published.edited_by, 'test@edx.org') self.assertLess(new_published.edited_on, pre_time) @@ -863,7 +870,7 @@ class TestCourseCreation(SplitModuleTest): Create a new course which overrides metadata and course_data """ pre_time = datetime.datetime.now(UTC) - original_locator = CourseLocator(course_id="contender", revision='draft') + original_locator = CourseLocator(course_id="contender", branch='draft') original = modulestore().get_course(original_locator) original_index = modulestore().get_course_index_info(original_locator) data_payload = {} @@ -902,7 +909,7 @@ class TestCourseCreation(SplitModuleTest): """ Test changing the org, pretty id, etc of a course. Test that it doesn't allow changing the id, etc. """ - locator = CourseLocator(course_id="GreekHero", revision='draft') + locator = CourseLocator(course_id="GreekHero", branch='draft') modulestore().update_course_index(locator, {'org': 'funkyU'}) course_info = modulestore().get_course_index_info(locator) self.assertEqual(course_info['org'], 'funkyU') @@ -938,7 +945,7 @@ class TestCourseCreation(SplitModuleTest): # an allowed but not recommended way to publish a course versions['published'] = self.GUID_D1 modulestore().update_course_index(locator, {'versions': versions}, update_versions=True) - course = modulestore().get_course(CourseLocator(course_id=locator.course_id, revision="published")) + course = modulestore().get_course(CourseLocator(course_id=locator.course_id, branch="published")) self.assertEqual(str(course.location.version_guid), self.GUID_D1) @@ -952,11 +959,11 @@ class TestInheritance(SplitModuleTest): """ # Note, not testing value where defined (course) b/c there's no # defined accessor for it on CourseDescriptor. - locator = BlockUsageLocator(course_id="GreekHero", usage_id="problem3_2", revision='draft') + locator = BlockUsageLocator(course_id="GreekHero", usage_id="problem3_2", branch='draft') node = modulestore().get_item(locator) # inherited self.assertEqual(node.graceperiod, datetime.timedelta(hours=2)) - locator = BlockUsageLocator(course_id="GreekHero", usage_id="problem1", revision='draft') + locator = BlockUsageLocator(course_id="GreekHero", usage_id="problem1", branch='draft') node = modulestore().get_item(locator) # overridden self.assertEqual(node.graceperiod, datetime.timedelta(hours=4)) diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py index 4c6c719224..b972d7c8eb 100644 --- a/common/lib/xmodule/xmodule/raw_module.py +++ b/common/lib/xmodule/xmodule/raw_module.py @@ -32,3 +32,22 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor): context=lines[line - 1][offset - 40:offset + 40], loc=self.location)) raise Exception, msg, sys.exc_info()[2] + + +class EmptyDataRawDescriptor(XmlDescriptor, XMLEditingDescriptor): + """ + Version of RawDescriptor for modules which may have no XML data, + but use XMLEditingDescriptor for import/export handling. + """ + data = String(default='', scope=Scope.content) + + @classmethod + def definition_from_xml(cls, xml_object, system): + if len(xml_object) == 0 and len(xml_object.items()) == 0: + return {'data': ''}, [] + return {'data': etree.tostring(xml_object, pretty_print=True, encoding='unicode')}, [] + + def definition_to_xml(self, resource_fs): + if self.data: + return etree.fromstring(self.data) + return etree.Element(self.category) diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index 2fe9d70627..fb1bed2d3a 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -445,7 +445,7 @@ class ImportTestCase(BaseCourseTestCase): render_string_from_sample_gst_xml = """ \ """.strip() - self.assertEqual(gst_sample.render, render_string_from_sample_gst_xml) + self.assertIn(render_string_from_sample_gst_xml, gst_sample.data) def test_word_cloud_import(self): modulestore = XMLModuleStore(DATA_DIR, course_dirs=['word_cloud']) diff --git a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py index 39dc6afe0e..90ff209c7d 100644 --- a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py +++ b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py @@ -66,7 +66,7 @@ class TestXBlockWrapper(object): @property def leaf_module_runtime(self): runtime = Mock() - runtime.render_template = lambda *args, **kwargs: unicode((args, kwargs)) + runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs) runtime.anonymous_student_id = 'dummy_anonymous_student_id' runtime.open_ended_grading_interface = {} runtime.seed = 5 @@ -78,7 +78,7 @@ class TestXBlockWrapper(object): @property def leaf_descriptor_runtime(self): runtime = Mock() - runtime.render_template = lambda *args, **kwargs: unicode((args, kwargs)) + runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs) return runtime def leaf_descriptor(self, descriptor_cls): @@ -102,7 +102,7 @@ class TestXBlockWrapper(object): @property def container_descriptor_runtime(self): runtime = Mock() - runtime.render_template = lambda *args, **kwargs: unicode((args, kwargs)) + runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs) return runtime def container_descriptor(self, descriptor_cls): diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 6c5640346e..763975fc3b 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -12,7 +12,7 @@ import time from django.http import Http404 from xmodule.x_module import XModule -from xmodule.raw_module import RawDescriptor +from xmodule.raw_module import EmptyDataRawDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor from xblock.core import Integer, Scope, String, Float, Boolean @@ -97,7 +97,7 @@ class VideoModule(VideoFields, XModule): class VideoDescriptor(VideoFields, MetadataOnlyEditingDescriptor, - RawDescriptor): + EmptyDataRawDescriptor): module_class = VideoModule def __init__(self, *args, **kwargs): @@ -130,19 +130,15 @@ class VideoDescriptor(VideoFields, _parse_video_xml(video, video.data) return video - def definition_to_xml(self, resource_fs): - """ - Override the base implementation. We don't actually have anything in the 'data' field - (it's an empty string), so we just return a simple XML element - """ - return etree.Element('video') - def _parse_video_xml(video, xml_data): """ Parse video fields out of xml_data. The fields are set if they are present in the XML. """ + if not xml_data: + return + xml = etree.fromstring(xml_data) display_name = xml.get('display_name') diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py index 004e6ed320..900bded760 100644 --- a/common/lib/xmodule/xmodule/word_cloud_module.py +++ b/common/lib/xmodule/xmodule/word_cloud_module.py @@ -10,7 +10,7 @@ import json import logging from pkg_resources import resource_string -from xmodule.raw_module import RawDescriptor +from xmodule.raw_module import EmptyDataRawDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.x_module import XModule @@ -240,7 +240,7 @@ class WordCloudModule(WordCloudFields, XModule): return self.content -class WordCloudDescriptor(WordCloudFields, MetadataOnlyEditingDescriptor, RawDescriptor): +class WordCloudDescriptor(WordCloudFields, MetadataOnlyEditingDescriptor, EmptyDataRawDescriptor): """Descriptor for WordCloud Xmodule.""" module_class = WordCloudModule template_dir_name = 'word_cloud' diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 65c9a0e981..d399001a6a 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -12,6 +12,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecif from xblock.core import XBlock, Scope, String, Integer, Float, ModelType from xblock.fragment import Fragment +from xblock.runtime import Runtime from xmodule.modulestore.locator import BlockUsageLocator log = logging.getLogger(__name__) @@ -870,7 +871,7 @@ class XMLParsingSystem(DescriptorSystem): self.policy = policy -class ModuleSystem(object): +class ModuleSystem(Runtime): ''' This is an abstraction such that x_modules can function independent of the courseware (e.g. import into other types of courseware, LMS, diff --git a/common/static/sass/_mixins.scss b/common/static/sass/_mixins.scss index d89f1d12d5..3687d395f4 100644 --- a/common/static/sass/_mixins.scss +++ b/common/static/sass/_mixins.scss @@ -205,7 +205,7 @@ // extends - UI archetypes - well .ui-well { box-shadow: inset 0 1px 2px 1px $shadow; - padding: ($baseline*0.75); + padding: ($baseline*0.75) $baseline; } // ==================== diff --git a/common/templates/courseware_vendor_js.html b/common/templates/courseware_vendor_js.html index 84e682ddac..20466210b8 100644 --- a/common/templates/courseware_vendor_js.html +++ b/common/templates/courseware_vendor_js.html @@ -8,6 +8,7 @@ + ## codemirror diff --git a/conf/locale/babel.cfg b/conf/locale/babel.cfg index 5b8333cf1e..6631586ad4 100644 --- a/conf/locale/babel.cfg +++ b/conf/locale/babel.cfg @@ -17,3 +17,5 @@ input_encoding = utf-8 input_encoding = utf-8 [mako: common/templates/**.html] input_encoding = utf-8 +[mako: cms/templates/emails/**.txt] +input_encoding = utf-8 diff --git a/conf/locale/config b/conf/locale/config index 58f8da0513..3a0b04adbb 100644 --- a/conf/locale/config +++ b/conf/locale/config @@ -1,4 +1,4 @@ { - "locales" : ["en", "es"], + "locales" : ["en", "zh_CN"], "dummy-locale" : "fr" } diff --git a/docs/internal/testing.md b/docs/internal/testing.md index cfca297f2b..b060336d1d 100644 --- a/docs/internal/testing.md +++ b/docs/internal/testing.md @@ -186,8 +186,8 @@ uses [Selenium](http://docs.seleniumhq.org/) to control the Chrome browser. **Prerequisite**: You must have [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver) installed to run the tests in Chrome. The tests are confirmed to run -with Chrome (not Chromium) version 26.0.0.1410.63 with ChromeDriver -version r195636. +with Chrome (not Chromium) version 28.0.1500.71 with ChromeDriver +version 2.1.210398. To run all the acceptance tests: diff --git a/lms/djangoapps/branding/views.py b/lms/djangoapps/branding/views.py index dd57e8d4d4..985dfa52d0 100644 --- a/lms/djangoapps/branding/views.py +++ b/lms/djangoapps/branding/views.py @@ -24,13 +24,13 @@ def index(request): from external_auth.views import ssl_login return ssl_login(request) if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE'): - return redirect(settings.MKTG_URLS.get('ROOT')) + return redirect(settings.MKTG_URLS.get('ROOT')) university = branding.get_university(request.META.get('HTTP_HOST')) if university is None: return student.views.index(request, user=request.user) - return courseware.views.university_profile(request, university) + return redirect('/') @ensure_csrf_cookie @@ -48,4 +48,4 @@ def courses(request): if university is None: return courseware.views.courses(request) - return courseware.views.university_profile(request, university) + return redirect('/') diff --git a/lms/djangoapps/course_wiki/tests/tests.py b/lms/djangoapps/course_wiki/tests/tests.py index 663d6b53b2..6bbd8011d6 100644 --- a/lms/djangoapps/course_wiki/tests/tests.py +++ b/lms/djangoapps/course_wiki/tests/tests.py @@ -90,8 +90,8 @@ class WikiRedirectTestCase(LoginEnrollmentTestCase): """ Ensure that the response has the course navigator. """ - self.assertTrue("course info" in resp.content.lower()) - self.assertTrue("courseware" in resp.content.lower()) + self.assertContains(resp, "Course Info") + self.assertContains(resp, "courseware") def test_course_navigator(self): """" diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index ef1b786645..91ec14011e 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -163,7 +163,7 @@ def get_course_about_section(course, section_key): html = '' if about_module is not None: - html = about_module.get_html() + html = about_module.runtime.render(about_module, None, 'student_view').content return html @@ -211,7 +211,7 @@ def get_course_info_section(request, course, section_key): html = '' if info_module is not None: - html = info_module.get_html() + html = info_module.runtime.render(info_module, None, 'student_view').content return html diff --git a/lms/djangoapps/courseware/features/homepage.py b/lms/djangoapps/courseware/features/homepage.py index 585d1582d7..51c3277e69 100644 --- a/lms/djangoapps/courseware/features/homepage.py +++ b/lms/djangoapps/courseware/features/homepage.py @@ -8,7 +8,7 @@ from nose.tools import assert_in, assert_equals @step(u'I should see the following Partners in the Partners section') def i_should_see_partner(step): partners = world.browser.find_by_css(".partner .name span") - names = set(span.text for span in partners) + names = set(span.html for span in partners) for partner in step.hashes: assert_in(partner['Partner'], names) diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 149542c344..8931f82724 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -91,6 +91,13 @@ def _discussion(tab, user, course, active_page): return [] +def _external_discussion(tab, user, course, active_page): + """ + This returns a tab that links to an external discussion service + """ + return [CourseTab('Discussion', tab['link'], active_page == 'discussion')] + + def _external_link(tab, user, course, active_page): # external links are never active return [CourseTab(tab['name'], tab['link'], False)] @@ -150,6 +157,12 @@ def _staff_grading(tab, user, course, active_page): return [] +def _syllabus(tab, user, course, active_page): + """Display the syllabus tab""" + link = reverse('syllabus', args=[course.id]) + return [CourseTab('Syllabus', link, active_page == 'syllabus')] + + def _peer_grading(tab, user, course, active_page): if user.is_authenticated(): @@ -216,6 +229,7 @@ VALID_TAB_TYPES = { 'course_info': TabImpl(need_name, _course_info), 'wiki': TabImpl(need_name, _wiki), 'discussion': TabImpl(need_name, _discussion), + 'external_discussion': TabImpl(key_checker(['link']), _external_discussion), 'external_link': TabImpl(key_checker(['name', 'link']), _external_link), 'textbooks': TabImpl(null_validator, _textbooks), 'pdf_textbooks': TabImpl(null_validator, _pdf_textbooks), @@ -225,7 +239,8 @@ VALID_TAB_TYPES = { 'peer_grading': TabImpl(null_validator, _peer_grading), 'staff_grading': TabImpl(null_validator, _staff_grading), 'open_ended': TabImpl(null_validator, _combined_open_ended_grading), - 'notes': TabImpl(null_validator, _notes_tab) + 'notes': TabImpl(null_validator, _notes_tab), + 'syllabus': TabImpl(null_validator, _syllabus) } @@ -371,6 +386,6 @@ def get_static_tab_contents(request, course, tab): html = '' if tab_module is not None: - html = tab_module.get_html() + html = tab_module.runtime.render(tab_module, None, 'student_view').content return html diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index 33c8d12701..cdb8bb8ea8 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -77,13 +77,15 @@ class BaseTestXmodule(ModuleStoreTestCase): data=self.DATA ) - system = get_test_system() - system.render_template = lambda template, context: context + self.runtime = get_test_system() + # Allow us to assert that the template was called in the same way from + # different code paths while maintaining the type returned by render_template + self.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items())) model_data = {'location': self.item_descriptor.location} model_data.update(self.MODEL_DATA) self.item_module = self.item_descriptor.module_class( - system, self.item_descriptor, model_data + self.runtime, self.item_descriptor, model_data ) self.item_url = Location(self.item_module.location).url() diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 6b409f677b..25056ba100 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -1,7 +1,7 @@ """ Test for lms courseware app, module render unit """ -from mock import MagicMock, patch +from mock import MagicMock, patch, Mock import json from django.http import Http404, HttpResponse @@ -12,8 +12,10 @@ from django.test.client import RequestFactory from django.test.utils import override_settings from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase import courseware.module_render as render -from courseware.tests.tests import LoginEnrollmentTestCase +from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MONGO_MODULESTORE from courseware.model_data import ModelDataCache from modulestore_config import TEST_DATA_XML_MODULESTORE @@ -49,8 +51,10 @@ class ModuleRenderTestCase(LoginEnrollmentTestCase): dispatch=self.dispatch)) def test_get_module(self): - self.assertIsNone(render.get_module('dummyuser', None, - 'invalid location', None, None)) + self.assertEqual( + None, + render.get_module('dummyuser', None, 'invalid location', None, None) + ) def test_module_render_with_jump_to_id(self): """ @@ -230,7 +234,8 @@ class TestTOC(TestCase): 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, None, model_data_cache) - assert reduce(lambda x, y: x and (y in actual), expected, True) + for toc_section in expected: + self.assertIn(toc_section, actual) def test_toc_toy_from_section(self): chapter = 'Overview' @@ -257,4 +262,109 @@ class TestTOC(TestCase): 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) actual = render.toc_for_course(self.portal_user, request, self.toy_course, chapter, section, model_data_cache) - assert reduce(lambda x, y: x and (y in actual), expected, True) + for toc_section in expected: + self.assertIn(toc_section, actual) + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestHtmlModifiers(ModuleStoreTestCase): + """ + Tests to verify that standard modifications to the output of XModule/XBlock + student_view are taking place + """ + def setUp(self): + self.user = UserFactory.create() + self.request = RequestFactory().get('/') + self.request.user = self.user + self.request.session = {} + self.course = CourseFactory.create() + self.content_string = '

        This is the content

        ' + self.rewrite_link = 'Test rewrite' + self.course_link = 'Test course rewrite' + self.descriptor = ItemFactory.create( + category='html', + data=self.content_string + self.rewrite_link + self.course_link + ) + self.location = self.descriptor.location + self.model_data_cache = ModelDataCache.cache_for_descriptor_descendents( + self.course.id, + self.user, + self.descriptor + ) + + def test_xmodule_display_wrapper_enabled(self): + module = render.get_module( + self.user, + self.request, + self.location, + self.model_data_cache, + self.course.id, + wrap_xmodule_display=True, + ) + result_fragment = module.runtime.render(module, None, 'student_view') + + self.assertIn('section class="xmodule_display xmodule_HtmlModule"', result_fragment.content) + + def test_xmodule_display_wrapper_disabled(self): + module = render.get_module( + self.user, + self.request, + self.location, + self.model_data_cache, + self.course.id, + wrap_xmodule_display=False, + ) + result_fragment = module.runtime.render(module, None, 'student_view') + + self.assertNotIn('section class="xmodule_display xmodule_HtmlModule"', result_fragment.content) + + def test_static_link_rewrite(self): + module = render.get_module( + self.user, + self.request, + self.location, + self.model_data_cache, + self.course.id, + ) + result_fragment = module.runtime.render(module, None, 'student_view') + + self.assertIn( + '/c4x/{org}/{course}/asset/foo_content'.format( + org=self.course.location.org, + course=self.course.location.course, + ), + result_fragment.content + ) + + def test_course_link_rewrite(self): + module = render.get_module( + self.user, + self.request, + self.location, + self.model_data_cache, + self.course.id, + ) + result_fragment = module.runtime.render(module, None, 'student_view') + + self.assertIn( + '/courses/{course_id}/bar/content'.format( + course_id=self.course.id + ), + result_fragment.content + ) + + @patch('courseware.module_render.has_access', Mock(return_value=True)) + def test_histogram(self): + module = render.get_module( + self.user, + self.request, + self.location, + self.model_data_cache, + self.course.id, + ) + result_fragment = module.runtime.render(module, None, 'student_view') + + self.assertIn( + 'Staff Debug', + result_fragment.content + ) diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py index d5afb1a78c..30cf556b9f 100644 --- a/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py +++ b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py @@ -34,9 +34,7 @@ class TestVideo(BaseTestXmodule): def test_videoalpha_constructor(self): """Make sure that all parameters extracted correclty from xml""" - # `get_html` return only context, cause we - # overwrite `system.render_template` - context = self.item_module.get_html() + fragment = self.runtime.render(self.item_module, None, 'student_view') expected_context = { 'data_dir': getattr(self, 'data_dir', None), 'caption_asset_path': '/c4x/MITx/999/asset/subs_', @@ -51,7 +49,7 @@ class TestVideo(BaseTestXmodule): 'youtube_streams': self.item_module.youtube_streams, 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) } - self.assertDictEqual(context, expected_context) + self.assertEqual(fragment.content, self.runtime.render_template('videoalpha.html', expected_context)) class TestVideoNonYouTube(TestVideo): @@ -78,9 +76,7 @@ class TestVideoNonYouTube(TestVideo): the template generates an empty string for the YouTube streams. """ - # `get_html` return only context, cause we - # overwrite `system.render_template` - context = self.item_module.get_html() + fragment = self.runtime.render(self.item_module, None, 'student_view') expected_context = { 'data_dir': getattr(self, 'data_dir', None), 'caption_asset_path': '/c4x/MITx/999/asset/subs_', @@ -95,4 +91,4 @@ class TestVideoNonYouTube(TestVideo): 'youtube_streams': '', 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) } - self.assertDictEqual(context, expected_context) + self.assertEqual(fragment.content, self.runtime.render_template('videoalpha.html', expected_context)) diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_xml.py b/lms/djangoapps/courseware/tests/test_videoalpha_xml.py index a14fc6cac6..6df957a0e5 100644 --- a/lms/djangoapps/courseware/tests/test_videoalpha_xml.py +++ b/lms/djangoapps/courseware/tests/test_videoalpha_xml.py @@ -104,10 +104,9 @@ class VideoAlphaModuleUnitTest(unittest.TestCase): def test_videoalpha_constructor(self): """Make sure that all parameters extracted correclty from xml""" module = VideoAlphaFactory.create() + module.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items())) - # `get_html` return only context, cause we - # overwrite `system.render_template` - context = module.get_html() + fragment = module.runtime.render(module, None, 'student_view') expected_context = { 'caption_asset_path': '/static/subs/', 'sub': module.sub, @@ -122,7 +121,7 @@ class VideoAlphaModuleUnitTest(unittest.TestCase): 'track': module.track, 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) } - self.assertDictEqual(context, expected_context) + self.assertEqual(fragment.content, module.runtime.render_template('videoalpha.html', expected_context)) self.assertDictEqual( json.loads(module.get_instance_state()), diff --git a/lms/djangoapps/courseware/tests/test_word_cloud.py b/lms/djangoapps/courseware/tests/test_word_cloud.py index 7c214f3458..6b01ae94f4 100644 --- a/lms/djangoapps/courseware/tests/test_word_cloud.py +++ b/lms/djangoapps/courseware/tests/test_word_cloud.py @@ -242,9 +242,7 @@ class TestWordCloud(BaseTestXmodule): def test_word_cloud_constructor(self): """Make sure that all parameters extracted correclty from xml""" - # `get_html` return only context, cause we - # overwrite `system.render_template` - context = self.item_module.get_html() + fragment = self.runtime.render(self.item_module, None, 'student_view') expected_context = { 'ajax_url': self.item_module.system.ajax_url, @@ -253,4 +251,4 @@ class TestWordCloud(BaseTestXmodule): 'num_inputs': 5, # default value 'submitted': False # default value } - self.assertDictEqual(context, expected_context) + self.assertEqual(fragment.content, self.runtime.render_template('word_cloud.html', expected_context)) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index fbe2c05ada..cd245d2610 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -120,9 +120,8 @@ class PageLoaderTestCase(LoginEnrollmentTestCase): self.assertEqual(response.redirect_chain[0][1], 302) if check_content: - unavailable_msg = "this module is temporarily unavailable" - self.assertEqual(response.content.find(unavailable_msg), -1) - self.assertFalse(isinstance(descriptor, ErrorDescriptor)) + self.assertNotContains(response, "this module is temporarily unavailable") + self.assertNotIsInstance(descriptor, ErrorDescriptor) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index b675f4dfc3..7815188227 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -400,7 +400,7 @@ def index(request, course_id, chapter=None, section=None, # add in the appropriate timer information to the rendering context: context.update(check_for_active_timelimit_module(request, course_id, course)) - context['content'] = section_module.get_html() + context['content'] = section_module.runtime.render(section_module, None, 'student_view').content else: # section is none, so display a message prev_section = get_current_child(chapter_module) @@ -632,57 +632,6 @@ def mktg_course_about(request, course_id): 'show_courseware_link': show_courseware_link}) - -@ensure_csrf_cookie -@cache_if_anonymous -def static_university_profile(request, org_id): - """ - Return the profile for the particular org_id that does not have any courses. - """ - # Redirect to the properly capitalized org_id - last_path = request.path.split('/')[-1] - if last_path != org_id: - return redirect('static_university_profile', org_id=org_id) - - # Render template - template_file = "university_profile/{0}.html".format(org_id).lower() - context = dict(courses=[], org_id=org_id) - return render_to_response(template_file, context) - - -@ensure_csrf_cookie -@cache_if_anonymous -def university_profile(request, org_id): - """ - Return the profile for the particular org_id. 404 if it's not valid. - """ - virtual_orgs_ids = settings.VIRTUAL_UNIVERSITIES - meta_orgs = getattr(settings, 'META_UNIVERSITIES', {}) - - # Get all the ids associated with this organization - all_courses = modulestore().get_courses() - valid_orgs_ids = set(c.org for c in all_courses) - valid_orgs_ids.update(virtual_orgs_ids + meta_orgs.keys()) - - if org_id not in valid_orgs_ids: - raise Http404("University Profile not found for {0}".format(org_id)) - - # Grab all courses for this organization(s) - org_ids = set([org_id] + meta_orgs.get(org_id, [])) - org_courses = [] - domain = request.META.get('HTTP_HOST') - for key in org_ids: - cs = get_courses_by_university(request.user, domain=domain)[key] - org_courses.extend(cs) - - org_courses = sort_by_announcement(org_courses) - - context = dict(courses=org_courses, org_id=org_id) - template_file = "university_profile/{0}.html".format(org_id).lower() - - return render_to_response(template_file, context) - - def render_notifications(request, course, notifications): context = { 'notifications': notifications, @@ -779,12 +728,16 @@ def submission_history(request, course_id, student_username, location): except StudentModule.DoesNotExist: return HttpResponse(escape("{0} has never accessed problem {1}".format(student_username, location))) - history_entries = StudentModuleHistory.objects.filter(student_module=student_module).order_by('-id') + history_entries = StudentModuleHistory.objects.filter( + student_module=student_module + ).order_by('-id') # If no history records exist, let's force a save to get history started. if not history_entries: student_module.save() - history_entries = StudentModuleHistory.objects.filter(student_module=student_module).order_by('-id') + history_entries = StudentModuleHistory.objects.filter( + student_module=student_module + ).order_by('-id') context = { 'history_entries': history_entries, diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 1875d380a6..b2f81639c9 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -21,6 +21,7 @@ from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from django.core.urlresolvers import reverse from django.core.mail import send_mail +from django.utils import timezone import xmodule.graders as xmgraders from xmodule.modulestore.django import modulestore @@ -93,6 +94,7 @@ def instructor_dashboard(request, course_id): 'title': 'Course Statistics At A Glance', } data = [['# Enrolled', CourseEnrollment.objects.filter(course_id=course_id).count()]] + data += [['Date', timezone.now().isoformat()]] data += compute_course_stats(course).items() if request.user.is_staff: for field in course.fields: diff --git a/lms/djangoapps/user_api/tests/test_views.py b/lms/djangoapps/user_api/tests/test_views.py index 075c1f0d9f..451b167050 100644 --- a/lms/djangoapps/user_api/tests/test_views.py +++ b/lms/djangoapps/user_api/tests/test_views.py @@ -1,3 +1,5 @@ +import base64 + from django.contrib.auth.models import User from django.test import TestCase from django.test.utils import override_settings @@ -31,6 +33,9 @@ class UserApiTestCase(TestCase): UserPreferenceFactory.create(user=self.users[1], key="key0") ] + def basic_auth(self, username, password): + return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))} + def request_with_auth(self, method, *args, **kwargs): """Issue a get request to the given URI with the API key header""" return getattr(self.client, method)(*args, HTTP_X_EDX_API_KEY=TEST_API_KEY, **kwargs) @@ -127,6 +132,15 @@ class UserViewSetTest(UserApiTestCase): def test_debug_auth(self): self.assertHttpOK(self.client.get(self.LIST_URI)) + @override_settings(DEBUG=False) + @override_settings(EDX_API_KEY=TEST_API_KEY) + def test_basic_auth(self): + # ensure that having basic auth headers in the mix does not break anything + self.assertHttpOK( + self.request_with_auth("get", self.LIST_URI, **self.basic_auth('someuser', 'somepass'))) + self.assertHttpForbidden( + self.client.get(self.LIST_URI, **self.basic_auth('someuser', 'somepass'))) + def test_get_list_empty(self): User.objects.all().delete() result = self.get_json(self.LIST_URI) diff --git a/lms/djangoapps/user_api/views.py b/lms/djangoapps/user_api/views.py index d4f19be099..c64a5a4d23 100644 --- a/lms/djangoapps/user_api/views.py +++ b/lms/djangoapps/user_api/views.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib.auth.models import User +from rest_framework import authentication from rest_framework import filters from rest_framework import permissions from rest_framework import viewsets @@ -25,6 +26,7 @@ class ApiKeyHeaderPermission(permissions.BasePermission): class UserViewSet(viewsets.ReadOnlyModelViewSet): + authentication_classes = (authentication.SessionAuthentication,) permission_classes = (ApiKeyHeaderPermission,) queryset = User.objects.all() serializer_class = UserSerializer @@ -33,6 +35,7 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet): + authentication_classes = (authentication.SessionAuthentication,) permission_classes = (ApiKeyHeaderPermission,) queryset = UserPreference.objects.all() filter_backends = (filters.DjangoFilterBackend,) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index fa490a244f..8d2ffba96e 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -178,13 +178,6 @@ for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items(): COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", []) -# automatic log in for load testing -MITX_FEATURES['AUTOMATIC_AUTH_FOR_LOAD_TESTING'] = ENV_TOKENS.get('AUTOMATIC_AUTH_FOR_LOAD_TESTING') -MITX_FEATURES['MAX_AUTO_AUTH_USERS'] = ENV_TOKENS.get('MAX_AUTO_AUTH_USERS') - -# discussion home panel must be explicitly enabled -MITX_FEATURES['ENABLE_DISCUSSION_HOME_PANEL'] = ENV_TOKENS.get('ENABLE_DISCUSSION_HOME_PANEL', False) - ############################## SECURE AUTH ITEMS ############### # Secret things: passwords, access keys, etc. diff --git a/lms/envs/common.py b/lms/envs/common.py index aa17d3f783..29e0de7d91 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -73,7 +73,7 @@ MITX_FEATURES = { 'ENABLE_DISCUSSION_SERVICE': True, # discussion home panel, which includes a subscription on/off setting for discussion digest emails. # this should remain off in production until digest notifications are online. - 'ENABLE_DISCUSSION_HOME_PANEL': True, + 'ENABLE_DISCUSSION_HOME_PANEL': False, 'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard) @@ -166,7 +166,7 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds ############################# SET PATH INFORMATION ############################# -PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/lms +PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/lms REPO_ROOT = PROJECT_ROOT.dirname() COMMON_ROOT = REPO_ROOT / "common" ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in @@ -320,8 +320,6 @@ CODE_JAIL = { 'limits': { # How many CPU seconds can jailed code use? 'CPU': 1, - # How large a file can jailed code write? - 'FSIZE': 50000, }, } @@ -383,6 +381,8 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html USE_I18N = True USE_L10N = True +# Localization strings (e.g. django.po) are under this directory +LOCALE_PATHS = (REPO_ROOT + '/conf/locale',) # edx-platform/conf/locale/ # Messages MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' @@ -488,6 +488,9 @@ MIDDLEWARE_CLASSES = ( 'course_wiki.course_nav.Middleware', + # Detects user-requested locale from 'accept-language' header in http request + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.transaction.TransactionMiddleware', # 'debug_toolbar.middleware.DebugToolbarMiddleware', diff --git a/lms/templates/admin_dashboard.html b/lms/templates/admin_dashboard.html index 6a903a3f94..5314881233 100644 --- a/lms/templates/admin_dashboard.html +++ b/lms/templates/admin_dashboard.html @@ -1,4 +1,5 @@ <%namespace name='static' file='static_content.html'/> +<%! from django.utils.translation import ugettext as _ %> <%inherit file="main.html" /> @@ -7,7 +8,7 @@

        -

        edX-wide Summary

        +

        ${_("{platform_name}-wide Summary").format(platform_name=settings.PLATFORM_NAME)}

        % for key in results["scalars"]: diff --git a/lms/templates/annotatable.html b/lms/templates/annotatable.html index f010305744..20a85d0ca2 100644 --- a/lms/templates/annotatable.html +++ b/lms/templates/annotatable.html @@ -1,3 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> +
        % if display_name is not UNDEFINED and display_name is not None: @@ -8,8 +10,8 @@ % if instructions_html is not UNDEFINED and instructions_html is not None:
        - Instructions - Collapse Instructions + ${_("Instructions")} + ${_("Collapse Instructions")}
        ${instructions_html} @@ -19,8 +21,8 @@
        - Guided Discussion - Hide Annotations + ${_("Guided Discussion")} + ${_("Hide Annotations")}
        ${content_html} diff --git a/lms/templates/combinedopenended/combined_open_ended.html b/lms/templates/combinedopenended/combined_open_ended.html index 5d8ef859aa..50f962d691 100644 --- a/lms/templates/combinedopenended/combined_open_ended.html +++ b/lms/templates/combinedopenended/combined_open_ended.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %>
        ${status|n} @@ -12,8 +13,8 @@ % endfor
        - - + +
        diff --git a/lms/templates/combinedopenended/combined_open_ended_legend.html b/lms/templates/combinedopenended/combined_open_ended_legend.html index e3e2494670..d5d482e190 100644 --- a/lms/templates/combinedopenended/combined_open_ended_legend.html +++ b/lms/templates/combinedopenended/combined_open_ended_legend.html @@ -1,6 +1,7 @@ +<%! from django.utils.translation import ugettext as _ %>
        - Legend + ${_("Legend")}
        % for i in xrange(0,len(legend_list)): <%legend_title=legend_list[i]['name'] %> diff --git a/lms/templates/combinedopenended/combined_open_ended_status.html b/lms/templates/combinedopenended/combined_open_ended_status.html index d13077737f..0369d6d9ff 100644 --- a/lms/templates/combinedopenended/combined_open_ended_status.html +++ b/lms/templates/combinedopenended/combined_open_ended_status.html @@ -1,7 +1,8 @@ +<%! from django.utils.translation import ugettext as _ %>
        - Status + ${_("Status")}
        %for i in xrange(0,len(status_list)): <%status=status_list[i]%> diff --git a/lms/templates/combinedopenended/open_ended_result_table.html b/lms/templates/combinedopenended/open_ended_result_table.html index 24bf7a76fe..bac684b91c 100644 --- a/lms/templates/combinedopenended/open_ended_result_table.html +++ b/lms/templates/combinedopenended/open_ended_result_table.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> % for co in context_list: % if co['grader_type'] in grader_type_image_dict: <%grader_type=co['grader_type']%> @@ -18,7 +19,7 @@ %if len(co['feedback'])>2:
        - See full feedback + ${_("See full feedback")}
        @@ -55,4 +56,4 @@

        %endif -%endfor \ No newline at end of file +%endfor diff --git a/lms/templates/combinedopenended/openended/open_ended.html b/lms/templates/combinedopenended/openended/open_ended.html index 909ef15838..15b3be1303 100644 --- a/lms/templates/combinedopenended/openended/open_ended.html +++ b/lms/templates/combinedopenended/openended/open_ended.html @@ -1,17 +1,18 @@ +<%! from django.utils.translation import ugettext as _ %>
        ${prompt|n}
        -

        Response

        +

        ${_("Response")}

        % if state == 'initial': - Unanswered + ${_("Unanswered")} % elif state == 'assessing': - Submitted for grading. + ${_("Submitted for grading.")} % if eta_message is not None: ${eta_message} % endif @@ -26,8 +27,8 @@
        - - + +
        diff --git a/lms/templates/combinedopenended/openended/open_ended_error.html b/lms/templates/combinedopenended/openended/open_ended_error.html index 58a90f86ef..65b7381d60 100644 --- a/lms/templates/combinedopenended/openended/open_ended_error.html +++ b/lms/templates/combinedopenended/openended/open_ended_error.html @@ -1,7 +1,8 @@ +<%! from django.utils.translation import ugettext as _ %>
        - There was an error with your submission. Please contact course staff. + ${_("There was an error with your submission. Please contact course staff.")}
        @@ -9,4 +10,4 @@ ${errors}
        -
        \ No newline at end of file +
        diff --git a/lms/templates/combinedopenended/openended/open_ended_evaluation.html b/lms/templates/combinedopenended/openended/open_ended_evaluation.html index da3f38b6a9..ee55120d51 100644 --- a/lms/templates/combinedopenended/openended/open_ended_evaluation.html +++ b/lms/templates/combinedopenended/openended/open_ended_evaluation.html @@ -1,23 +1,24 @@ +<%! from django.utils.translation import ugettext as _ %>
        ${msg|n}
        - Respond to Feedback + ${_("Respond to Feedback")}
        -

        How accurate do you find this feedback?

        +

        ${_("How accurate do you find this feedback?")}

          -
        • -
        • -
        • -
        • -
        • +
        • +
        • +
        • +
        • +
        -

        Additional comments:

        +

        ${_("Additional comments:")}

        - +
        -
        \ No newline at end of file +
        diff --git a/lms/templates/combinedopenended/openended/open_ended_rubric.html b/lms/templates/combinedopenended/openended/open_ended_rubric.html index 144cd829d9..f1d6abb8fa 100644 --- a/lms/templates/combinedopenended/openended/open_ended_rubric.html +++ b/lms/templates/combinedopenended/openended/open_ended_rubric.html @@ -1,6 +1,7 @@ +<%! from django.utils.translation import ugettext as _ %>
        -

        Rubric

        -

        Select the criteria you feel best represents this submission in each category.

        +

        ${_("Rubric")}

        +

        ${_("Select the criteria you feel best represents this submission in each category.")}

        % for i in range(len(categories)): <% category = categories[i] %> diff --git a/lms/templates/combinedopenended/selfassessment/self_assessment_hint.html b/lms/templates/combinedopenended/selfassessment/self_assessment_hint.html index 8c6eacba11..abdc25b77b 100644 --- a/lms/templates/combinedopenended/selfassessment/self_assessment_hint.html +++ b/lms/templates/combinedopenended/selfassessment/self_assessment_hint.html @@ -1,6 +1,7 @@ +<%! from django.utils.translation import ugettext as _ %>
        - Please enter a hint below: + ${_("Please enter a hint below:")}
        diff --git a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html index 5347e23844..3cc73fc657 100644 --- a/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html +++ b/lms/templates/combinedopenended/selfassessment/self_assessment_prompt.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %>
        @@ -5,7 +6,7 @@ ${prompt}
        -

        Response

        +

        ${_("Response")}

        @@ -19,5 +20,5 @@
        - + diff --git a/lms/templates/contact.html b/lms/templates/contact.html index a8f5e6b732..cb35aca359 100644 --- a/lms/templates/contact.html +++ b/lms/templates/contact.html @@ -1,13 +1,15 @@ +<%! from django.utils.translation import ugettext as _ %> + <%namespace name='static' file='static_content.html'/> <%inherit file="main.html" />
        @@ -15,21 +17,35 @@
        -

        Class Feedback

        -

        We are always seeking feedback to improve our courses. If you are an enrolled student and have any questions, feedback, suggestions, or any other issues specific to a particular class, please post on the discussion forums of that class.

        +

        ${_("Class Feedback")}

        +

        ${_("We are always seeking feedback to improve our courses. If you are an enrolled student and have any questions, feedback, suggestions, or any other issues specific to a particular class, please post on the discussion forums of that class.")}

        -

        General Inquiries and Feedback

        -

        "If you have a general question about edX please email info@edx.org. To see if your question has already been answered, visit our FAQ page. You can also join the discussion on our facebook page. Though we may not have a chance to respond to every email, we take all feedback into consideration.

        +

        ${_("General Inquiries and Feedback")}

        +

        ${_('If you have a general question about {platform_name} please email {contact_email}. To see if your question has already been answered, visit our {faq_link_start}FAQ page{faq_link_end}. You can also join the discussion on our {fb_link_start}facebook page{fb_link_end}. Though we may not have a chance to respond to every email, we take all feedback into consideration.').format( + platform_name=settings.PLATFORM_NAME, + contact_email=settings.CONTACT_EMAIL, + faq_link_start=''.format(url=reverse('faq_edx')), + faq_link_end='', + fb_link_start=''. + fb_link_end='' + )}

        -

        Technical Inquiries and Feedback

        -

        If you have suggestions/feedback about the overall edX platform, or are facing general technical issues with the platform (e.g., issues with email addresses and passwords), you can reach us at technical@edx.org. For technical questions, please make sure you are using a current version of Firefox or Chrome, and include browser and version in your e-mail, as well as screenshots or other pertinent details. If you find a bug or other issues, you can reach us at the following: bugs@edx.org.

        +

        ${_("Technical Inquiries and Feedback")}

        +

        ${_('If you have suggestions/feedback about the overall {platform_name} platform, or are facing general technical issues with the platform (e.g., issues with email addresses and passwords), you can reach us at {tech_email}. For technical questions, please make sure you are using a current version of Firefox or Chrome, and include browser and version in your e-mail, as well as screenshots or other pertinent details. If you find a bug or other issues, you can reach us at the following: {bugs_email}.').format( + tech_email=settings.TECH_SUPPORT_EMAIL, + bug_email=settings.BUGS_EMAIL, + platform_name=settings.PLATFORM_NAME + )}

        -

        Media

        -

        Please visit our media/press page for more information. 
For any media or press inquiries, please email press@edx.org.

        - -

        Universities

        -

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

        +

        ${_("Media")}

        +

        ${_('Please visit our {link_start}media/press page{link_end} for more information. For any media or press inquiries, please email {email}.').format( + link_start=''.format(url=reverse('faq_edx')), + link_end='', + email='press@edx.org', + )}

        +

        ${_("Universities")}

        +

        ${_('If you are a university wishing to collaborate with or if you have questions about {platform_name}, please email {email}.'.format(email='university@edx.org', platform_name="edX")}

        diff --git a/lms/templates/course.html b/lms/templates/course.html index e3dd9baf43..ddbae2c715 100644 --- a/lms/templates/course.html +++ b/lms/templates/course.html @@ -1,13 +1,14 @@ <%namespace name='static' file='static_content.html'/> <%namespace file='main.html' import="stanford_theme_enabled"/> <%! - from django.core.urlresolvers import reverse - from courseware.courses import course_image_url, get_course_about_section +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from courseware.courses import course_image_url, get_course_about_section %> <%page args="course" />
        %if course.is_newish: - New + ${_("New")} %endif diff --git a/lms/templates/course_filter.html b/lms/templates/course_filter.html deleted file mode 100644 index 9e7c0a16f4..0000000000 --- a/lms/templates/course_filter.html +++ /dev/null @@ -1,50 +0,0 @@ -
        - -
        diff --git a/lms/templates/course_groups/cohort_management.html b/lms/templates/course_groups/cohort_management.html index 239863beeb..4c9ca9e5a2 100644 --- a/lms/templates/course_groups/cohort_management.html +++ b/lms/templates/course_groups/cohort_management.html @@ -1,21 +1,22 @@ +<%! from django.utils.translation import ugettext as _ %>
        -

        Cohort groups

        +

        ${_("Cohort groups")}

        @@ -27,10 +28,10 @@

        - Add users by username or email. One per line or comma-separated. + ${_("Add users by username or email. One per line or comma-separated.")}

        - Add cohort members + ${_("Add cohort members")}
        diff --git a/lms/templates/course_groups/debug.html b/lms/templates/course_groups/debug.html index d8bbc324de..7554557f81 100644 --- a/lms/templates/course_groups/debug.html +++ b/lms/templates/course_groups/debug.html @@ -1,6 +1,8 @@ +<%! from django.utils.translation import ugettext as _ %> + ## "edX" should not be translated <%block name="title">edX diff --git a/lms/templates/courseware/accordion.html b/lms/templates/courseware/accordion.html index 5b9c6f7450..4761408232 100644 --- a/lms/templates/courseware/accordion.html +++ b/lms/templates/courseware/accordion.html @@ -1,9 +1,20 @@ -<%! from django.core.urlresolvers import reverse %> -<%! from xmodule.util.date_utils import get_default_time_display %> +<%! + from django.core.urlresolvers import reverse + from xmodule.util.date_utils import get_default_time_display + from django.utils.translation import ugettext as _ +%> <%def name="make_chapter(chapter)">
        -

        + <% + if chapter.get('active'): + aria_label = _('{chapter}, current chapter').format(chapter=chapter['display_name']) + active_class = ' class="active"' + else: + aria_label = chapter['display_name'] + active_class = '' + %> +

        ${chapter['display_name']} diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index 15317de207..b1bc715e9a 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse from courseware.courses import course_image_url, get_course_about_section @@ -65,7 +66,7 @@ -<%block name="title">About ${course.number} +<%block name="title">${_("About {course.number}").format(course=course)}
        @@ -76,7 +77,7 @@

        ${course.number}: ${get_course_about_section(course, "title")} % if not self.theme_enabled(): - ${get_course_about_section(course, "university")} + ${get_course_about_section(course, "university")} % endif

        @@ -86,13 +87,13 @@ %if show_courseware_link: %endif - You are registered for this course (${course.number}) + ${_("You are registered for this course {course.number}").format(course=course)} %if show_courseware_link: - View Courseware + ${_("View Courseware")} %endif %else: - Register for ${course.number} + ${_("Register for {course.number}").format(course=course)}
        %endif

        @@ -115,16 +116,16 @@
        - +
        @@ -136,7 +137,7 @@
          -
        1. Course Number

          ${course.number}
        2. -
        3. Classes Start

          ${course.start_date_text}
        4. +
        5. ${_("Course Number")}

          ${course.number}
        6. +
        7. ${_("Classes Start")}

          ${course.start_date_text}
        8. ## We plan to ditch end_date (which is not stored in course metadata), ## but for backwards compatibility, show about/end_date blob if it exists. % if get_course_about_section(course, "end_date") or course.end:
        9. -

          Classes End

          +

          ${_("Classes End")}

          % if get_course_about_section(course, "end_date"): ${get_course_about_section(course, "end_date")} % else: @@ -180,13 +181,13 @@ % endif % if get_course_about_section(course, "effort"): -
        10. Estimated Effort

          ${get_course_about_section(course, "effort")}
        11. +
        12. ${_("Estimated Effort")}

          ${get_course_about_section(course, "effort")}
        13. % endif - ##
        14. Course Length

          15 weeks
        15. + ##
        16. ${_('Course Length')}

          ${_('{number} weeks').format(number=15)}
        17. % if get_course_about_section(course, "prerequisites"): -
        18. Prerequisites

          ${get_course_about_section(course, "prerequisites")}
        19. +
        20. ${_("Prerequisites")}

          ${get_course_about_section(course, "prerequisites")}
        21. % endif
        @@ -196,10 +197,11 @@ % if get_course_about_section(course, "ocw_links"):
        -

        Additional Resources

        +

        ${_("Additional Resources")}

        + ## "MITOpenCourseware" should *not* be translated

        MITOpenCourseware

        ${get_course_about_section(course, "ocw_links")}
        @@ -215,10 +217,10 @@
        - +
        - +
        diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html index 98329b9836..799b10b36b 100644 --- a/lms/templates/courseware/course_navigation.html +++ b/lms/templates/courseware/course_navigation.html @@ -12,6 +12,7 @@ def url_class(is_active): return "" %> <%! from courseware.tabs import get_course_tabs %> +<%! from django.utils.translation import ugettext as _ %>
        - ## I'm removing this for now since we aren't using it for the fall. - ## <%include file="course_filter.html" />
          %for course in courses: diff --git a/lms/templates/courseware/courseware-error.html b/lms/templates/courseware/courseware-error.html index e289e1c99d..f0f7969026 100644 --- a/lms/templates/courseware/courseware-error.html +++ b/lms/templates/courseware/courseware-error.html @@ -1,7 +1,9 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="/main.html" /> <%namespace name='static' file='../static_content.html'/> <%block name="bodyclass">courseware -<%block name="title">Courseware – edX +## Translators: "edX" should *not* be translated +<%block name="title">${_("Courseware")} - ${settings.PLATFORM_NAME} <%block name="headextra"> <%static:css group='course'/> @@ -11,7 +13,7 @@
          -

          There has been an error on the edX servers

          -

          We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at technical@edx.org to report any problems or downtime.

          +

          ${_('There has been an error on the {span_start}{platform_name}{span_end} servers').format(platform_name=settings.PLATFORM_NAME, span_start='', span_end='')}

          +

          ${_("We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at '{tech_support_email}' to report any problems or downtime.").format(tech_support_email=settings.TECH_SUPPORT_EMAIL)}

          diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index e009e535e3..8d033434f0 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -1,7 +1,8 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="/main.html" /> <%namespace name='static' file='/static_content.html'/> <%block name="bodyclass">courseware ${course.css_class} -<%block name="title">${course.number} Courseware +<%block name="title">${_("{course_number} Courseware").format(course_number=course.number)} <%block name="headextra"> <%static:css group='course'/> @@ -155,7 +156,7 @@
          % if timer_navigation_return_url: - Return to Exam + ${_("Return to Exam")} % endif
          Time Remaining:
           
          @@ -170,9 +171,9 @@
          % if accordion: -
          +
          - close + ${_("close")}
        - ## `news` should be `None` whenever a non-edX theme is enabled: - ## see common/djangoapps/student/views.py#_get_news - %if news: - - %endif
        -

        Current Courses

        +

        ${_("Current Courses")}

        % if len(courses) > 0: @@ -211,11 +140,11 @@ % if course.id in show_courseware_links_for: - ${course.number} ${course.display_name_with_default} Cover Image + ${_('{course_number} {course_name} Cover Image').format(course_number='${course.number}', course_name='${course.display_name_with_default}')} % else:
        - ${course.number} ${course.display_name_with_default} Cover Image + ${_('{course_number} {course_name} Cover Image').format(course_number='${course.number}', course_name='${course.display_name_with_default}')}
        % endif @@ -223,11 +152,11 @@

        % if course.has_ended(): - Course Completed - ${course.end_date_text} + ${_("Course Completed - {end_date}").format(end_date=course.end_date_text)} % elif course.has_started(): - Course Started - ${course.start_date_text} + ${_("Course Started - {start_date}").format(start_date=course.start_date_text)} % else: # hasn't started yet - Course Starts - ${course.start_date_text} + ${_("Course Starts - {start_date}").format(start_date=course.start_date_text)} % endif

        ${get_course_about_section(course, 'university')}

        @@ -249,27 +178,41 @@ % if registration is None and testcenter_exam_info.is_registering():
        - Register for Pearson exam -

        Registration for the Pearson exam is now open and will close on ${testcenter_exam_info.registration_end_date_text}

        + ${_("Register for Pearson exam")} +

        ${_("Registration for the Pearson exam is now open and will close on {end_date}").format(end_date="{}".format(testcenter_exam_info.registration_end_date_text))}

        % endif % if registration is not None: % if registration.is_accepted:
        - Schedule Pearson exam -

        Registration number: ${registration.client_candidate_id}

        -

        Write this down! You’ll need it to schedule your exam.

        + ${_("Schedule Pearson exam")} +

        ${_("{link_start}Registration{link_end} number: {number}").format( + link_start=''.format(url=testcenter_register_target), + link_end='', + number=registration.client_candidate_id, + )}

        +

        ${_("Write this down! You'll need it to schedule your exam.")}

        % endif % if registration.is_rejected:
        -

        Your registration for the Pearson exam has been rejected. Please see your registration status details. Otherwise contact edX at exam-help@edx.org for further help.

        +

        + ${_("Your registration for the Pearson exam has been rejected. Please {link_start}see your registration status details{link_end}.").format( + link_start=''.format(url=testcenter_register_target), + link_end='')} + ${_("Otherwise {link_start}contact edX at {email}{link_end} for further help.").format( + link_start=''.format(email="exam-help@edx.org", about=get_course_about_section(course, 'university'), number=course.number), + link_end='', + email="exam-help@edx.org", + )}

        % endif % if not registration.is_accepted and not registration.is_rejected:
        -

        Your registration for the Pearson exam is pending. Within a few days, you should see a confirmation number here, which can be used to schedule your exam.

        +

        ${_("Your {link_start}registration for the Pearson exam{link_end} is pending.").format(link_start=''.format(url=testcenter_register_target), link_end='')} + ${_("Within a few days, you should see a confirmation number here, which can be used to schedule your exam.")} +

        % endif % endif @@ -292,17 +235,16 @@
        % if cert_status['status'] == 'processing': -

        Final course details are being wrapped up at - this time. Your final standing will be available shortly.

        +

        ${_("Final course details are being wrapped up at this time. Your final standing will be available shortly.")}

        % elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'): -

        Your final grade: +

        ${_("Your final grade:")} ${"{0:.0f}%".format(float(cert_status['grade'])*100)}. % if cert_status['status'] == 'notpassing': - Grade required for a certificate: + ${_("Grade required for a certificate:")} ${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}. % elif cert_status['status'] == 'restricted':

        - Your certificate is being held pending confirmation that the issuance of your certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting ${settings.CONTACT_EMAIL}. + ${_("Your certificate is being held pending confirmation that the issuance of your certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {email}.").format(email='{email}.'.format(email=settings.CONTACT_EMAIL))}

        % endif

        @@ -312,17 +254,17 @@ % endif @@ -332,12 +274,12 @@ % if course.id in show_courseware_links_for: % if course.has_ended(): - View Archived Course + ${_('View Archived Course')} % else: - View Course + ${_('View Course')} % endif % endif - Unregister + ${_('Unregister')}
        @@ -346,16 +288,16 @@ % endfor % else:
        -

        Looks like you haven't registered for any courses yet.

        +

        ${_("Looks like you haven't registered for any courses yet.")}

        - Find courses now! + ${_("Find courses now!")}
        % endif % if staff_access and len(errored_courses) > 0:
        -

        Course-loading errors

        +

        ${_("Course-loading errors")}

        % for course_dir, errors in errored_courses.items():

        ${course_dir | h}

        @@ -374,7 +316,7 @@
        % endif + % endif
        diff --git a/lms/templates/discussion/_user_profile.html b/lms/templates/discussion/_user_profile.html index 1ab0f2fee7..2e803a31ce 100644 --- a/lms/templates/discussion/_user_profile.html +++ b/lms/templates/discussion/_user_profile.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%! from django_comment_client.helpers import pluralize %> <%! from django_comment_client.permissions import has_permission, check_permissions_by_view %> <%! from operator import attrgetter %> @@ -15,9 +16,9 @@ % if check_permissions_by_view(user, course.id, content=None, name='update_moderator_status'): % if "Moderator" in role_names: - Revoke Moderator rights + ${_("Revoke Moderator rights")} % else: - Promote to Moderator + ${_("Promote to Moderator")} % endif % endif
        diff --git a/lms/templates/discussion/index.html b/lms/templates/discussion/index.html index f0cb18d617..3b6937b7d5 100644 --- a/lms/templates/discussion/index.html +++ b/lms/templates/discussion/index.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%! import django_comment_client.helpers as helpers %> <%! from django.template.defaultfilters import escapejs %> <%! from django.core.urlresolvers import reverse %> @@ -5,7 +6,7 @@ <%inherit file="../main.html" /> <%namespace name='static' file='../static_content.html'/> <%block name="bodyclass">discussion -<%block name="title">Discussion – ${course.number | h} +<%block name="title">${_("Discussion - {course_number}").format(course_number=course.number) | h} <%block name="headextra"> <%static:css group='course'/> diff --git a/lms/templates/discussion/maintenance.html b/lms/templates/discussion/maintenance.html index a0f57cdfb3..c5f73c02ba 100644 --- a/lms/templates/discussion/maintenance.html +++ b/lms/templates/discussion/maintenance.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="../main.html" /> -

        We're sorry

        -

        The forums are currently undergoing maintenance. We'll have them back up shortly!

        +

        ${_("We're sorry")}

        +

        ${_("The forums are currently undergoing maintenance. We'll have them back up shortly!")}

        diff --git a/lms/templates/discussion/single_thread.html b/lms/templates/discussion/single_thread.html index 642dfcf46d..6aae08a7a7 100644 --- a/lms/templates/discussion/single_thread.html +++ b/lms/templates/discussion/single_thread.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%! import django_comment_client.helpers as helpers %> <%! from django.template.defaultfilters import escapejs %> <%! from django.core.urlresolvers import reverse %> @@ -6,7 +7,7 @@ <%inherit file="../main.html" /> <%namespace name='static' file='../static_content.html'/> <%block name="bodyclass">discussion -<%block name="title">Discussion – ${course.number} +<%block name="title">${_("Discussion - {course_number}").format(course_number=course.number) | h} <%block name="headextra"> <%static:css group='course'/> diff --git a/lms/templates/discussion/user_profile.html b/lms/templates/discussion/user_profile.html index 3b273f15fd..1c1901c10b 100644 --- a/lms/templates/discussion/user_profile.html +++ b/lms/templates/discussion/user_profile.html @@ -1,9 +1,10 @@ +<%! from django.utils.translation import ugettext as _ %> <%! from django.template.defaultfilters import escapejs %> <%inherit file="../main.html" /> <%namespace name='static' file='../static_content.html'/> <%block name="bodyclass">discussion -<%block name="title">Discussion – ${course.number | h} +<%block name="title">${_("Discussion - {course_number}").format(course_number=course.number) | h} <%block name="headextra"> <%static:css group='course'/> @@ -19,7 +20,7 @@
        -
        diff --git a/lms/templates/email_change_failed.html b/lms/templates/email_change_failed.html index 0f077ec769..aff4fd734f 100644 --- a/lms/templates/email_change_failed.html +++ b/lms/templates/email_change_failed.html @@ -1,13 +1,15 @@ +<%! from django.utils.translation import ugettext as _ %> + <%inherit file="main.html" />
        -

        E-mail change failed

        +

        ${_("E-mail change failed")}


        -

        We were unable to send a confirmation email to ${email}

        +

        ${_("We were unable to send a confirmation email to {email}").format(email=email)}

        -

        Go back to the home page.

        +

        ${_('Go back to the {link_start}home page{link_end}.').format(link_start='', link_end='')}

        diff --git a/lms/templates/email_change_successful.html b/lms/templates/email_change_successful.html index 01c45d7050..1fae973931 100644 --- a/lms/templates/email_change_successful.html +++ b/lms/templates/email_change_successful.html @@ -1,12 +1,17 @@ +<%! from django.utils.translation import ugettext as _ %> + <%! from django.core.urlresolvers import reverse %> <%inherit file="main.html" />
        -

        E-mail change successful!

        +

        ${_("E-mail change successful!")}


        -

        You should see your new email in your dashboard.

        +

        ${_('You should see your new email in your {link_start}dashboard{link_end}.').format( + link_start=''.format(url=reverse('dashboard')), + link_end='', + )}

        diff --git a/lms/templates/email_exists.html b/lms/templates/email_exists.html index e1d12bba96..15df3e76b7 100644 --- a/lms/templates/email_exists.html +++ b/lms/templates/email_exists.html @@ -1,13 +1,15 @@ +<%! from django.utils.translation import ugettext as _ %> + <%inherit file="main.html" />
        -

        E-mail change failed

        +

        ${_("E-mail change failed")}


        -

        An account with the new e-mail address already exists.

        +

        ${_("An account with the new e-mail address already exists.")}

        -

        Go back to the home page.

        +

        ${_("Go back to the {link_start}home page{link_end}.").format(link_start='', link_end='')}

        diff --git a/lms/templates/emails_change_successful.html b/lms/templates/emails_change_successful.html index 34a5c6f0f2..da622c696a 100644 --- a/lms/templates/emails_change_successful.html +++ b/lms/templates/emails_change_successful.html @@ -1,12 +1,17 @@ +<%! from django.utils.translation import ugettext as _ %> + <%! from django.core.urlresolvers import reverse %> <%inherit file="main.html" />
        -

        E-mail change successful!

        +

        ${_("E-mail change successful!")}


        -

        You should see your new email in your dashboard.

        +

        ${_('You should see your new email in your {link_start}dashboard{link_end}.').format( + link_start=''.format(url=reverse('dashboard')), + link_end='', + )}

        diff --git a/lms/templates/enroll_students.html b/lms/templates/enroll_students.html index 2f0b6ccc01..c3c4f5111b 100644 --- a/lms/templates/enroll_students.html +++ b/lms/templates/enroll_students.html @@ -1,29 +1,31 @@ -

        Student Enrollment Form

        +<%! from django.utils.translation import ugettext as _ %> -

        Course: ${ course } +

        ${_("Student Enrollment Form")}

        + +

        ${_("Course: ") % course}

        -

        Add new students

        +

        ${_("Add new students")}

        -

        Existing students: +

        ${_("Existing students:")}

        ${ existing_students } -

        New students added: +

        ${_("New students added: ")} ${ added_students } -

        Students rejected: +

        ${_("Students rejected: ")} ${ rejected_students } -

        Debug: +

        ${_("Debug: ")}

        ${ debug } -

        foo -

        bar -

        biff +

        ${_("foo")} +

        ${_("bar")} +

        ${_("biff")} diff --git a/lms/templates/extauth_failure.html b/lms/templates/extauth_failure.html index 330c63e604..adc680a7de 100644 --- a/lms/templates/extauth_failure.html +++ b/lms/templates/extauth_failure.html @@ -1,11 +1,13 @@ +<%! from django.utils.translation import ugettext as _ %> + - External Authentication failed + ${_("External Authentication failed")} -

        External Authentication failed

        +

        ${_("External Authentication failed")}

        ${message}

        diff --git a/lms/templates/folditbasic.html b/lms/templates/folditbasic.html index fc161ee45d..e05ceb1452 100644 --- a/lms/templates/folditbasic.html +++ b/lms/templates/folditbasic.html @@ -1,24 +1,26 @@ +<%! from django.utils.translation import ugettext as _ %> + <%! from xmodule.util.date_utils import get_default_time_display %>
        -

        Due: ${get_default_time_display(due)} +

        ${_("Due:")} ${get_default_time_display(due)}

        - Status: + ${_("Status:")} % if success: - You have successfully gotten to level ${goal_level}. + ${_('You have successfully gotten to level {goal_level}.').format(goal_level=goal_level)}' % else: - You have not yet gotten to level ${goal_level}. + ${_('You have not yet gotten to level {goal_level}.').format(goal_level=goal_level)} % endif

        -

        Completed puzzles

        +

        ${_("Completed puzzles")}

        - - + + % for puzzle in completed: diff --git a/lms/templates/folditchallenge.html b/lms/templates/folditchallenge.html index 677bc286c8..36e8f0caee 100644 --- a/lms/templates/folditchallenge.html +++ b/lms/templates/folditchallenge.html @@ -1,10 +1,12 @@ +<%! from django.utils.translation import ugettext as _ %> +
        -

        Puzzle Leaderboard

        +

        ${_("Puzzle Leaderboard")}

        LevelSubmitted${_("Level")}${_("Submitted")}
        - - + + % for pair in top_scores: diff --git a/lms/templates/footer.html b/lms/templates/footer.html index daad0a2457..9c94e5751e 100644 --- a/lms/templates/footer.html +++ b/lms/templates/footer.html @@ -1,5 +1,6 @@ ## mako <%! from django.core.urlresolvers import reverse %> +<%! from django.utils.translation import ugettext as _ %> <%namespace name='static' file='static_content.html'/> @@ -73,15 +74,15 @@ - + diff --git a/lms/templates/forgot_password_modal.html b/lms/templates/forgot_password_modal.html index 0f88b88f97..e4c0c02e33 100644 --- a/lms/templates/forgot_password_modal.html +++ b/lms/templates/forgot_password_modal.html @@ -1,30 +1,32 @@ +<%! from django.utils.translation import ugettext as _ %> + <%! from django.core.urlresolvers import reverse %> - <%block name="js_extra"> - @@ -90,46 +92,46 @@

        - Please provide the following information to log into your ${settings.PLATFORM_NAME} account. Required fields are noted by bold text and an asterisk (*). + ${_('Please provide the following information to log into your {platform_name} account. Required fields are noted by bold text and an asterisk (*).').format(platform_name=settings.PLATFORM_NAME)}

        - Required Information + ${_('Required Information')}
        1. - +
        2. - + - Forgot password? + ${_('Forgot password?')}
        - Account Preferences + ${_('Account Preferences')}
        1. - +
        @@ -147,27 +149,27 @@
        UserScore${_("User")}${_("Score")}
        % for s in students: @@ -34,8 +36,8 @@ function name_deny(id) { - + % endfor
        ${s['new_name']|h} ${s['email']|h} ${s['rationale']|h}[Confirm] - [Reject]
        [${_("Confirm")}] + ${_("[Reject]")}
        diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index a26e1ca367..589d12666d 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -3,6 +3,7 @@ <%namespace file='main.html' import="login_query, stanford_theme_enabled"/> <%! from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ # App that handles subdomain specific branding import branding @@ -34,16 +35,16 @@ site_status_msg = get_site_status_msg(course_id) % if course: -
        +
        % else: -
        +
        % endif

    @@ -84,24 +85,24 @@ site_status_msg = get_site_status_msg(course_id) <%block name="navigation_global_links"> % if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE'): % endif % if not settings.MITX_FEATURES['DISABLE_LOGIN_BUTTON']: % if course and settings.MITX_FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain: % else: % endif % endif @@ -111,9 +112,9 @@ site_status_msg = get_site_status_msg(course_id) @@ -122,7 +123,7 @@ site_status_msg = get_site_status_msg(course_id) % if course: -
    Warning: Your browser is not fully supported. We strongly recommend using Chrome or Firefox.
    +
    ${_('Warning: Your browser is not fully supported. We strongly recommend using {chrome_link_start}Chrome{chrome_link_end} or {ff_link_start}Firefox{ff_link_end}.').format(chrome_link_start='', chrome_link_end='', ff_link_start='', ff_link_end='')}
    % endif %if not user.is_authenticated(): diff --git a/lms/templates/notes.html b/lms/templates/notes.html index 3fea6faa3e..16fdc2ebc9 100644 --- a/lms/templates/notes.html +++ b/lms/templates/notes.html @@ -1,3 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + <%namespace name='static' file='static_content.html'/> <%inherit file="main.html" /> <%! @@ -55,27 +57,24 @@
    -

    My Notes

    +

    ${_("My Notes")}

    % for note in notes:
    ${note.quote|h}
    ${note.text.replace("\n", "
    ") | n,h}
      % if note.tags: -
    • Tags: ${note.tags|h}
    • +
    • ${_("Tags: {tags}").format(tags=note.tags) | h}
    • % endif -
    • Author: ${note.user.username}
    • -
    • Created: ${note.created.strftime('%m/%d/%Y %H:%m')}
    • -
    • Source: ${note.uri|h}
    • +
    • ${_('Author: {username}').format(username=note.user.username)}
    • +
    • ${_('Created: {datetime}').format(datetime=note.created.strftime('%m/%d/%Y %H:%m'))}
    • +
    • ${_('Source: {link}').format(link='{url}'.format(url=note.uri))}
    % endfor % if notes is UNDEFINED or len(notes) == 0: -

    You do not have any notes.

    +

    ${_('You do not have any notes.')}

    % endif
    - - - diff --git a/lms/templates/open_ended_problems/combined_notifications.html b/lms/templates/open_ended_problems/combined_notifications.html index deb66b6064..86eb4083dd 100644 --- a/lms/templates/open_ended_problems/combined_notifications.html +++ b/lms/templates/open_ended_problems/combined_notifications.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="/main.html" /> <%block name="bodyclass">${course.css_class} <%namespace name='static' file='/static_content.html'/> @@ -6,7 +7,7 @@ <%static:css group='course'/> -<%block name="title">${course.number} Combined Notifications +<%block name="title">${_("{course_number} Combined Notifications").format(course_number=course.number)} <%include file="/courseware/course_navigation.html" args="active_page='open_ended'" /> @@ -14,13 +15,13 @@
    ${error_text}
    -

    Open Ended Console

    -

    Instructions

    -

    Here are items that could potentially need your attention.

    +

    ${_("Open Ended Console")}

    +

    ${_("Instructions")}

    +

    ${_("Here are items that could potentially need your attention.")}

    % if success: % if len(notification_list) == 0:
    - No items require attention at the moment. + ${_("No items require attention at the moment.")}
    %else:
    diff --git a/lms/templates/open_ended_problems/open_ended_flagged_problems.html b/lms/templates/open_ended_problems/open_ended_flagged_problems.html index b4c6f43685..ab60e54300 100644 --- a/lms/templates/open_ended_problems/open_ended_flagged_problems.html +++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="/main.html" /> <%block name="bodyclass">${course.css_class} <%namespace name='static' file='/static_content.html'/> @@ -6,7 +7,7 @@ <%static:css group='course'/> -<%block name="title">${course.number} Flagged Open Ended Problems +<%block name="title">${_("{course_number} Flagged Open Ended Problems").format(course_number=course.number)} <%include file="/courseware/course_navigation.html" args="active_page='open_ended_flagged_problems'" /> @@ -17,19 +18,19 @@
    ${error_text}
    -

    Flagged Open Ended Problems

    -

    Instructions

    -

    Here are a list of open ended problems for this course that have been flagged by students as potentially inappropriate.

    +

    ${_("Flagged Open Ended Problems")}

    +

    ${_("Instructions")}

    +

    ${_("Here are a list of open ended problems for this course that have been flagged by students as potentially inappropriate.")}

    % if success: % if len(problem_list) == 0:
    - No flagged problems exist. + ${_("No flagged problems exist.")}
    %else: - - + + @@ -42,10 +43,10 @@ ${problem['student_response']}
    NameResponse${_("Name")}${_("Response")}
    - Unflag + ${_("Unflag")} - Ban + ${_("Ban")}
    diff --git a/lms/templates/open_ended_problems/open_ended_problems.html b/lms/templates/open_ended_problems/open_ended_problems.html index 3709fb2de6..56b269d8b7 100644 --- a/lms/templates/open_ended_problems/open_ended_problems.html +++ b/lms/templates/open_ended_problems/open_ended_problems.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%inherit file="/main.html" /> <%block name="bodyclass">${course.css_class} <%namespace name='static' file='/static_content.html'/> @@ -6,7 +7,7 @@ <%static:css group='course'/> -<%block name="title">${course.number} Open Ended Problems +<%block name="title">${_("{course_number} Open Ended Problems").format(course_number=course.number)} <%include file="/courseware/course_navigation.html" args="active_page='open_ended_problems'" /> @@ -14,31 +15,31 @@
    ${error_text}
    -

    Open Ended Problems

    -

    Instructions

    -

    Here are a list of open ended problems for this course.

    +

    ${_("Open Ended Problems")}

    +

    ${_("Instructions")}

    +

    ${_("Here are a list of open ended problems for this course.")}

    % if success: % if len(problem_list) == 0:
    - You have not attempted any open ended problems yet. + ${_("You have not attempted any open ended problems yet.")}
    %else: - - - - + + + + %for problem in problem_list: -
    Problem NameStatusGrader TypeETA${_("Problem Name")}${_("Status")}${_("Grader Type")}${_("ETA")}
    - ${problem['problem_name']} + ${problem['problem_name']} - ${problem['state']} + ${problem['state']} + ${problem['grader_type']} diff --git a/lms/templates/peer_grading/peer_grading.html b/lms/templates/peer_grading/peer_grading.html index 0485b698b2..f423de1c6b 100644 --- a/lms/templates/peer_grading/peer_grading.html +++ b/lms/templates/peer_grading/peer_grading.html @@ -1,24 +1,25 @@ +<%! from django.utils.translation import ugettext as _ %>
    ${error_text}
    -

    Peer Grading

    -

    Instructions

    -

    Here are a list of problems that need to be peer graded for this course.

    +

    ${_("Peer Grading")}

    +

    ${_("Instructions")}

    +

    ${_("Here are a list of problems that need to be peer graded for this course.")}

    % if success: % if len(problem_list) == 0:
    - Nothing to grade! + ${_("Nothing to grade!")}
    %else:
    - - - - - - + + + + + + %for problem in problem_list: @@ -33,7 +34,7 @@ % if problem['due']: ${problem['due']} % else: - No due date + ${_("No due date")} % endif
    Problem NameDue dateGradedAvailableRequiredProgress${_("Problem Name")}${_("Due date")}${_("Graded")}${_("Available")}${_("Required")}${_("Progress")}
    diff --git a/lms/templates/peer_grading/peer_grading_closed.html b/lms/templates/peer_grading/peer_grading_closed.html index 712ad8b380..af5b606674 100644 --- a/lms/templates/peer_grading/peer_grading_closed.html +++ b/lms/templates/peer_grading/peer_grading_closed.html @@ -1,10 +1,9 @@ +<%! from django.utils.translation import ugettext as _ %>
    -

    Peer Grading

    -

    The due date has passed, and +

    ${_("Peer Grading")}

    % if use_for_single_location: - peer grading for this problem is closed at this time. +

    ${_("The due date has passed, and peer grading for this problem is closed at this time.")}

    %else: - peer grading is closed at this time. +

    ${_("The due date has passed, and peer grading is closed at this time.")}

    %endif -

    diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html index 5ad3136815..d99e14c706 100644 --- a/lms/templates/peer_grading/peer_grading_problem.html +++ b/lms/templates/peer_grading/peer_grading_problem.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %>
    @@ -5,15 +6,15 @@
    -

    Learning to Grade

    +

    ${_("Learning to Grade")}

    -

    Peer Grading

    +

    ${_("Peer Grading")}

    -

    Prompt (Hide)

    +

    ${_('Prompt')} ${_('(Hide)')}

    @@ -25,7 +26,7 @@
    -

    Student Response

    +

    ${_("Student Response")}

    @@ -39,17 +40,17 @@

    -

    Written Feedback

    -

    Please include some written feedback as well.

    +

    ${_("Written Feedback")}

    +

    ${_("Please include some written feedback as well.")}

    -
    This submission has explicit or pornographic content :
    -
    I do not know how to grade this question :
    +
    ${_("This submission has explicit or pornographic content : ")}
    +
    ${_("I do not know how to grade this question : ")}
    - +
    - +
    @@ -60,41 +61,41 @@
    -

    How did I do?

    +

    ${_("How did I do?")}

    - +
    -

    Ready to grade!

    -

    You have finished learning to grade, which means that you are now ready to start grading.

    - +

    ${_("Ready to grade!")}

    +

    ${_("You have finished learning to grade, which means that you are now ready to start grading.")}

    +
    -

    Learning to grade

    -

    You have not yet finished learning to grade this problem.

    -

    You will now be shown a series of instructor-scored essays, and will be asked to score them yourself.

    -

    Once you can score the essays similarly to an instructor, you will be ready to grade your peers.

    - +

    ${_("Learning to grade")}

    +

    ${_("You have not yet finished learning to grade this problem.")}

    +

    ${_("You will now be shown a series of instructor-scored essays, and will be asked to score them yourself.")}

    +

    ${_("Once you can score the essays similarly to an instructor, you will be ready to grade your peers.")}

    +
    -

    Are you sure that you want to flag this submission?

    +

    ${_("Are you sure that you want to flag this submission?")}

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

    - - + +
    - +
    diff --git a/lms/templates/problem.html b/lms/templates/problem.html index f4f8e78b66..efcb868cb9 100644 --- a/lms/templates/problem.html +++ b/lms/templates/problem.html @@ -1,3 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + <%namespace name='static' file='static_content.html'/>

    ${ problem['name'] } @@ -16,17 +18,17 @@ % endif % if reset_button: - + % endif % if save_button: - + % endif % if answer_available: - + % endif % if attempts_allowed :
    - You have used ${ attempts_used } of ${ attempts_allowed } submissions + ${_("You have used {num_used} of {num_total} submissions").format(num_used=attempts_used, num_total=attempts_allowed)}
    % endif

    diff --git a/lms/templates/provider_login.html b/lms/templates/provider_login.html index a98b2ab32b..3bcd22fafd 100644 --- a/lms/templates/provider_login.html +++ b/lms/templates/provider_login.html @@ -1,3 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + <%inherit file="main.html" /> <%namespace name='static' file='static_content.html'/> @@ -32,19 +34,19 @@
    @@ -262,23 +268,23 @@ % endif
    - +
    diff --git a/lms/templates/registration/activate_account_notice.html b/lms/templates/registration/activate_account_notice.html index ca051902b1..b65a36e273 100644 --- a/lms/templates/registration/activate_account_notice.html +++ b/lms/templates/registration/activate_account_notice.html @@ -1,3 +1,3 @@ -

    Thanks For Registering!

    -

    Your account is not active yet. An activation link has been sent to ${ email }, along with -instructions for activating your account.

    +<%! from django.utils.translation import ugettext as _ %> +

    ${_("Thanks For Registering!")}

    +

    ${_("Your account is not active yet. An activation link has been sent to {email}, along with instructions for activating your account.").format(email="{}".format(email))}

    diff --git a/lms/templates/registration/activation_complete.html b/lms/templates/registration/activation_complete.html index 7eb805e730..95f508d132 100644 --- a/lms/templates/registration/activation_complete.html +++ b/lms/templates/registration/activation_complete.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> @@ -7,23 +8,23 @@
    %if not already_active: -

    Activation Complete!

    +

    ${_("Activation Complete!")}

    %else: -

    Account already active!

    +

    ${_("Account already active!")}

    %endif
    - +

    %if not already_active: - Thanks for activating your account. + ${_("Thanks for activating your account.")} %else: - This account has already been activated. + ${_("This account has already been activated.")} %endif - + %if user_logged_in: - Visit your dashboard to see your courses. + ${_("Visit your {link_start}dashboard{link_end} to see your courses.").format(link_start=''.format(url=reverse('dashboard')), link_end='')} %else: - You can now log in. + ${_("You can now {link_start}log in{link_end}.").format(link_start=''.format(url=reverse('signin_user')), link_end='')} %endif

    diff --git a/lms/templates/registration/activation_invalid.html b/lms/templates/registration/activation_invalid.html index 0a6d6d30c9..edeede84e7 100644 --- a/lms/templates/registration/activation_invalid.html +++ b/lms/templates/registration/activation_invalid.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> @@ -6,14 +7,14 @@
    -

    Activation Invalid

    +

    ${_("Activation Invalid")}


    -

    Something went wrong. Check to make sure the URL you went to was - correct -- e-mail programs will sometimes split it into two - lines. If you still have issues, e-mail us to let us know what happened - at ${settings.BUGS_EMAIL}.

    +

    ${_('Something went wrong. Check to make sure the URL you went to was ' + 'correct -- e-mail programs will sometimes split it into two ' + 'lines. If you still have issues, e-mail us to let us know what happened ' + 'at {email}.').format(email='{email}'.format(email=settings.BUGS_EMAIL))}

    -

    Or you can go back to the home page.

    +

    ${_('Or you can go back to the {link_start}home page{link_end}.').format(link_start='', link_end='')}

    diff --git a/lms/templates/registration/login.html b/lms/templates/registration/login.html index 70e58965a4..d8fb92855e 100644 --- a/lms/templates/registration/login.html +++ b/lms/templates/registration/login.html @@ -1,28 +1,28 @@ +<%! from django.utils.translation import ugettext as _ %> {% extends "registration/base.html" %} -{% block title %}Log in{% endblock %} +{% block title %}${_("Log in")}{% endblock %} {% block content %} -

    Log in

    +

    ${_("Log in")}

    {% if form.errors %} -

    Please correct the errors below:

    +

    ${_("Please correct the errors below:")}

    {% endif %}
    {% csrf_token %}
    -
    {% if form.username.errors %} {{ form.username.errors|join:", " }}{% endif %}
    +
    {% if form.username.errors %} {{ form.username.errors|join:", " }}{% endif %}
    {{ form.username }}
    -
    {% if form.password.errors %} {{ form.password.errors|join:", " }}{% endif %}
    +
    {% if form.password.errors %} {{ form.password.errors|join:", " }}{% endif %}
    {{ form.password }}
    -
    +
    {% endblock %} {% block content-related %} -

    If you don't have an account, you can sign -up for one. +

    ${_("If you don't have an account, you can {link_start}sign up{link_end} for one.").format(link_start='', link_end='')} {% endblock %} diff --git a/lms/templates/registration/logout.html b/lms/templates/registration/logout.html index 3275d2e1b4..a3ff64507a 100644 --- a/lms/templates/registration/logout.html +++ b/lms/templates/registration/logout.html @@ -1,8 +1,9 @@ +<%! from django.utils.translation import ugettext as _ %> {% extends "registration/base.html" %} -{% block title %}Logged out{% endblock %} +{% block title %}${_("Logged out")}{% endblock %/} {% block content %} -

    You've been logged out.

    -

    Thanks for stopping by; when you come back, don't forget to log in again.

    -{% endblock %} \ No newline at end of file +

    ${_("You've been logged out.")}

    +

    ${_("Thanks for stopping by; when you come back, don't forget to {link_start}log in{link_end} again.").format(link_start='', link_end='')}

    +{% endblock %} diff --git a/lms/templates/registration/password_reset_complete.html b/lms/templates/registration/password_reset_complete.html index 3847f615b9..3f301102b5 100644 --- a/lms/templates/registration/password_reset_complete.html +++ b/lms/templates/registration/password_reset_complete.html @@ -1,3 +1,4 @@ +<%! from django.utils.translation import ugettext as _ %> {% load i18n %} {% load compressed %} {% load staticfiles %} @@ -5,7 +6,7 @@ - Your Password Reset is Complete + ${_("Your Password Reset is Complete")} {% compressed_css 'application' %} @@ -53,13 +54,13 @@
    -

    Your Password Reset is Complete

    +

    ${_("Your Password Reset is Complete")}

    {% block content %}
    -

    Your password has been set. You may go ahead and log in now..

    +

    ${_('Your password has been set. You may go ahead and {link_start}log in{link_end} now.').format(link_start='', link_end='')}

    {% endblock %}
    diff --git a/lms/templates/registration/password_reset_confirm.html b/lms/templates/registration/password_reset_confirm.html index 5809408dad..6a568545d1 100644 --- a/lms/templates/registration/password_reset_confirm.html +++ b/lms/templates/registration/password_reset_confirm.html @@ -1,10 +1,11 @@ +<%! from django.utils.translation import ugettext as _ %> {% load compressed %} {% load staticfiles %} - Reset Your edX Password + ${_("Reset Your {platform_name} Password").format(platform_name=settings.PLATFORM_NAME)} {% compressed_css 'application' %} @@ -52,79 +53,78 @@
    -

    Reset Your edX Password

    +

    ${_("Reset Your {platform_name} Password").format(platform_name=settings.PLATFORM_NAME)}

    {% if validlink %}
    -

    Password Reset Form

    +

    ${_("Password Reset Form")}

    {% csrf_token %}

    - Please enter your new password twice so we can verify you typed it in correctly.
    - Required fields are noted by bold text and an asterisk (*). + ${_('Please enter your new password twice so we can verify you typed it in correctly.
    ' + 'Required fields are noted by bold text and an asterisk (*).')}

    - Required Information + ${_("Required Information")}
    1. - +
    2. - +
    - +
    {% else %}
    -

    Your Password Reset Was Unsuccessful

    +

    ${_("Your Password Reset Was Unsuccessful")}

    -

    The password reset link was invalid, possibly because the link has already been used. Please return to the login page and start the password reset process again.

    +

    ${_('The password reset link was invalid, possibly because the link has already been used. Please return to the login page and start the password reset process again.')}

    {% endif %}
    diff --git a/lms/templates/registration/password_reset_done.html b/lms/templates/registration/password_reset_done.html index 0b029a854f..fa34ab6e19 100644 --- a/lms/templates/registration/password_reset_done.html +++ b/lms/templates/registration/password_reset_done.html @@ -1,8 +1,9 @@ +<%! from django.utils.translation import ugettext as _ %>
    -

    Password reset successful

    +

    ${_("Password reset successful")}


    -

    We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly.

    +

    ${_("We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly.")}

    diff --git a/lms/templates/registration/registration_complete.html b/lms/templates/registration/registration_complete.html index 9f0cea41db..d6f9a5659a 100644 --- a/lms/templates/registration/registration_complete.html +++ b/lms/templates/registration/registration_complete.html @@ -1,8 +1,9 @@ {% extends "registration/base.html" %} +{% load i18n %} -{% block title %}Registration complete{% endblock %} +{% block title %}{% trans "Registration complete" %}{% endblock %} {% block content %} -

    Check your email

    -

    An activation link has been sent to the email address you supplied, along with instructions for activating your account.

    +

    {% trans Check your email %}

    +

    {% trans "An activation link has been sent to the email address you supplied, along with instructions for activating your account."%}

    {% endblock %} \ No newline at end of file diff --git a/lms/templates/registration/registration_form.html b/lms/templates/registration/registration_form.html index 1e9d630639..f90392d73f 100644 --- a/lms/templates/registration/registration_form.html +++ b/lms/templates/registration/registration_form.html @@ -1,58 +1,59 @@ +<%! from django.utils.translation import ugettext as _ %> {% extends "registration/base.html" %} -{% block title %}Sign up{% endblock %} +{% block title %}${_("Sign up")}{% endblock %} {% block content %} {% if form.errors %} -

    Please correct the errors below: {{ form.non_field_errors }}

    +

    ${_("Please correct the errors below: {{ form.non_field_errors }}")}

    {% endif %} -

    Create an account

    - +

    ${_("Create an account")}

    +
    {% csrf_token %}

    - + {% if form.username.errors %}

    {{ form.username.errors.as_text }}

    {% endif %} {{ form.username }}

    - + {% if form.email.errors %}

    {{ form.email.errors.as_text }}

    {% endif %} {{ form.email }}

    - + {% if form.password1.errors %}

    {{ form.password1.errors.as_text }}

    {% endif %} {{ form.password1 }}

    - + {% if form.password2.errors %}

    {{ form.password2.errors.as_text }}

    {% endif %} {{ form.password2 }}

    -

    +

    - + {% endblock %} {% block content-related %} -

    Fill out the form to the left (all fields are required), and your -account will be created; you'll be sent an email with instructions on how -to finish your registration.

    +

    ${_("Fill out the form to the left (all fields are required), and your " +"account will be created; you'll be sent an email with instructions on how " +"to finish your registration.")}

    -

    We'll only use your email to send you signup instructions. We hate spam -as much as you do.

    +

    ${_("We'll only use your email to send you signup instructions. We hate spam " +"as much as you do.")}

    -

    This account will let you log into the ticket tracker, claim tickets, -and be exempt from spam filtering.

    +

    ${_("This account will let you log into the ticket tracker, claim tickets, " +"and be exempt from spam filtering")}.

    {% endblock %} diff --git a/lms/templates/seq_module.html b/lms/templates/seq_module.html index fff1279cd6..bb04d1b31c 100644 --- a/lms/templates/seq_module.html +++ b/lms/templates/seq_module.html @@ -1,7 +1,9 @@ +<%! from django.utils.translation import ugettext as _ %> +
    -
    diff --git a/lms/templates/signup_modal.html b/lms/templates/signup_modal.html index 9c1a868e2d..a9d709ba60 100644 --- a/lms/templates/signup_modal.html +++ b/lms/templates/signup_modal.html @@ -1,3 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + <%namespace name='static' file='static_content.html'/> <%! from django.core.urlresolvers import reverse %> <%! from django_countries.countries import COUNTRIES %> @@ -9,7 +11,7 @@
    -

    Sign Up for edX

    +

    ${_('Sign Up for {span_start}{platform_name}{span_end}').format(span_start='', span_end='', platform_name=settings.PLATFORM_NAME)}


    @@ -20,41 +22,41 @@
    % if has_extauth_info is UNDEFINED: - - - - + + + + - - - - - - + + + + + + % else: -

    Welcome ${extauth_id}


    -

    Enter a public username:

    - - - +

    ${_('Welcome {name}').format(name=extauth_id)}


    +

    ${_('Enter a public username:')}

    + + + % if ask_for_email: - - + + % endif - + % if ask_for_fullname: - - + + % endif - + % endif
    - +
    @@ -78,7 +80,7 @@
    - +
    - - + +
    @@ -101,33 +103,35 @@
    - +
    % if has_extauth_info is UNDEFINED: % endif
    - +

    @@ -146,7 +150,7 @@ $("[data-field='"+json.field+"']").addClass('field-error') } }); - + // removing close link's default behavior $('#login-modal .close-modal').click(function(e) { e.preventDefault(); diff --git a/lms/templates/staff_problem_info.html b/lms/templates/staff_problem_info.html index d24d6528ac..6d1517c447 100644 --- a/lms/templates/staff_problem_info.html +++ b/lms/templates/staff_problem_info.html @@ -1,3 +1,5 @@ +<%! from django.utils.translation import ugettext as _ %> + ## The JS for this is defined in xqa_interface.html ${module_content} %if location.category in ['problem','video','html','combinedopenended','graphical_slider_tool']: @@ -14,27 +16,27 @@ ${module_content} % endif
    % endif - + % if settings.MITX_FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW') and \ location.category == 'problem': - + % endif