diff --git a/cms/djangoapps/contentstore/__init__.py b/cms/djangoapps/contentstore/__init__.py index e8dccbbf60..8b13789179 100644 --- a/cms/djangoapps/contentstore/__init__.py +++ b/cms/djangoapps/contentstore/__init__.py @@ -1,3 +1 @@ -from xmodule.templates import update_templates -update_templates() diff --git a/cms/djangoapps/contentstore/features/section.feature b/cms/djangoapps/contentstore/features/section.feature index ad00ba2911..75e7a4af10 100644 --- a/cms/djangoapps/contentstore/features/section.feature +++ b/cms/djangoapps/contentstore/features/section.feature @@ -11,6 +11,14 @@ Feature: Create Section And I see a release date for my section And I see a link to create a new subsection + Scenario: Add a new section (with a quote in the name) to a course (bug #216) + Given I have opened a new course in Studio + When I click the New Section link + And I enter a section name with a quote and click save + Then I see my section name with a quote on the Courseware page + And I click to edit the section name + Then I see the complete section name with a quote in the editor + Scenario: Edit section release date Given I have opened a new course in Studio And I have added a new section diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py index ca67c477fb..cfa4e4bb52 100644 --- a/cms/djangoapps/contentstore/features/section.py +++ b/cms/djangoapps/contentstore/features/section.py @@ -1,5 +1,6 @@ from lettuce import world, step from common import * +from nose.tools import assert_equal ############### ACTIONS #################### @@ -12,10 +13,12 @@ def i_click_new_section_link(step): @step('I enter the section name and click save$') def i_save_section_name(step): - name_css = '.new-section-name' - save_css = '.new-section-name-save' - css_fill(name_css, 'My Section') - css_click(save_css) + save_section_name('My Section') + + +@step('I enter a section name with a quote and click save$') +def i_save_section_name_with_quote(step): + save_section_name('Section with "Quote"') @step('I have added a new section$') @@ -45,8 +48,24 @@ def i_save_a_new_section_release_date(step): @step('I see my section on the Courseware page$') def i_see_my_section_on_the_courseware_page(step): - section_css = 'span.section-name-span' - assert_css_with_text(section_css, 'My Section') + see_my_section_on_the_courseware_page('My Section') + + +@step('I see my section name with a quote on the Courseware page$') +def i_see_my_section_name_with_quote_on_the_courseware_page(step): + see_my_section_on_the_courseware_page('Section with "Quote"') + + +@step('I click to edit the section name$') +def i_click_to_edit_section_name(step): + css_click('span.section-name-span') + + +@step('I see the complete section name with a quote in the editor$') +def i_see_complete_section_name_with_quote_in_editor(step): + css = '.edit-section-name' + assert world.browser.is_element_present_by_css(css, 5) + assert_equal(world.browser.find_by_css(css).value, 'Section with "Quote"') @step('the section does not exist$') @@ -88,3 +107,17 @@ def the_section_release_date_is_updated(step): css = 'span.published-status' status_text = world.browser.find_by_css(css).text assert status_text == 'Will Release: 12/25/2013 at 12:00am' + + +############ HELPER METHODS ################### + +def save_section_name(name): + name_css = '.new-section-name' + save_css = '.new-section-name-save' + css_fill(name_css, name) + css_click(save_css) + + +def see_my_section_on_the_courseware_page(name): + section_css = 'span.section-name-span' + assert_css_with_text(section_css, name) \ No newline at end of file diff --git a/cms/djangoapps/contentstore/features/subsection.feature b/cms/djangoapps/contentstore/features/subsection.feature index 5acb5bfe44..4b5f5b869d 100644 --- a/cms/djangoapps/contentstore/features/subsection.feature +++ b/cms/djangoapps/contentstore/features/subsection.feature @@ -9,6 +9,14 @@ Feature: Create Subsection And I enter the subsection name and click save Then I see my subsection on the Courseware page + Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216) + Given I have opened a new course section in Studio + When I click the New Subsection link + And I enter a subsection name with a quote and click save + Then I see my subsection name with a quote on the Courseware page + And I click to edit the subsection name + Then I see the complete subsection name with a quote in the editor + Scenario: Delete a subsection Given I have opened a new course section in Studio And I have added a new subsection diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py index e2041b8dbf..88e1424898 100644 --- a/cms/djangoapps/contentstore/features/subsection.py +++ b/cms/djangoapps/contentstore/features/subsection.py @@ -1,5 +1,6 @@ from lettuce import world, step from common import * +from nose.tools import assert_equal ############### ACTIONS #################### @@ -20,28 +21,60 @@ def i_click_the_new_subsection_link(step): @step('I enter the subsection name and click save$') def i_save_subsection_name(step): - name_css = 'input.new-subsection-name-input' - save_css = 'input.new-subsection-name-save' - css_fill(name_css, 'Subsection One') - css_click(save_css) + save_subsection_name('Subsection One') + + +@step('I enter a subsection name with a quote and click save$') +def i_save_subsection_name_with_quote(step): + save_subsection_name('Subsection With "Quote"') + + +@step('I click to edit the subsection name$') +def i_click_to_edit_subsection_name(step): + css_click('span.subsection-name-value') + + +@step('I see the complete subsection name with a quote in the editor$') +def i_see_complete_subsection_name_with_quote_in_editor(step): + css = '.subsection-display-name-input' + assert world.browser.is_element_present_by_css(css, 5) + assert_equal(world.browser.find_by_css(css).value, 'Subsection With "Quote"') @step('I have added a new subsection$') def i_have_added_a_new_subsection(step): add_subsection() + ############ ASSERTIONS ################### @step('I see my subsection on the Courseware page$') def i_see_my_subsection_on_the_courseware_page(step): - css = 'span.subsection-name' - assert world.browser.is_element_present_by_css(css) - css = 'span.subsection-name-value' - assert_css_with_text(css, 'Subsection One') + see_subsection_name('Subsection One') + + +@step('I see my subsection name with a quote on the Courseware page$') +def i_see_my_subsection_name_with_quote_on_the_courseware_page(step): + see_subsection_name('Subsection With "Quote"') @step('the subsection does not exist$') def the_subsection_does_not_exist(step): css = 'span.subsection-name' assert world.browser.is_element_not_present_by_css(css) + + +############ HELPER METHODS ################### + +def save_subsection_name(name): + name_css = 'input.new-subsection-name-input' + save_css = 'input.new-subsection-name-save' + css_fill(name_css, name) + css_click(save_css) + +def see_subsection_name(name): + css = 'span.subsection-name' + assert world.browser.is_element_present_by_css(css) + css = 'span.subsection-name-value' + assert_css_with_text(css, name) diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py index e3cac7de02..789226db1a 100644 --- a/cms/djangoapps/contentstore/management/commands/delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/delete_course.py @@ -17,8 +17,7 @@ from auth.authz import _delete_course_group class Command(BaseCommand): - help = \ -'''Delete a MongoDB backed course''' + help = '''Delete a MongoDB backed course''' def handle(self, *args, **options): if len(args) != 1 and len(args) != 2: @@ -28,19 +27,19 @@ class Command(BaseCommand): commit = False if len(args) == 2: - commit = args[1] == 'commit' + commit = args[1] == 'commit' if commit: - print 'Actually going to delete the course from DB....' + print 'Actually going to delete the course from DB....' ms = modulestore('direct') cs = contentstore() if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"): - if query_yes_no("Are you sure. This action cannot be undone!", default="no"): - loc = CourseDescriptor.id_to_location(loc_str) - if delete_course(ms, cs, loc, commit) == True: - print 'removing User permissions from course....' - # in the django layer, we need to remove all the user permissions groups associated with this course - if commit: - _delete_course_group(loc) + if query_yes_no("Are you sure. This action cannot be undone!", default="no"): + loc = CourseDescriptor.id_to_location(loc_str) + if delete_course(ms, cs, loc, commit) == True: + print 'removing User permissions from course....' + # in the django layer, we need to remove all the user permissions groups associated with this course + if commit: + _delete_course_group(loc) diff --git a/cms/djangoapps/contentstore/management/commands/prompt.py b/cms/djangoapps/contentstore/management/commands/prompt.py index 211c48406c..40a39d0a11 100644 --- a/cms/djangoapps/contentstore/management/commands/prompt.py +++ b/cms/djangoapps/contentstore/management/commands/prompt.py @@ -13,7 +13,7 @@ def query_yes_no(question, default="yes"): """ valid = {"yes":True, "y":True, "ye":True, "no":False, "n":False} - if default == None: + if default is None: prompt = " [y/n] " elif default == "yes": prompt = " [Y/n] " diff --git a/cms/djangoapps/contentstore/management/commands/update_templates.py b/cms/djangoapps/contentstore/management/commands/update_templates.py new file mode 100644 index 0000000000..b30d30480a --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/update_templates.py @@ -0,0 +1,9 @@ +from xmodule.templates import update_templates +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + help = \ +'''Imports and updates the Studio component templates from the code pack and put in the DB''' + + def handle(self, *args, **options): + update_templates() \ No newline at end of file diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py deleted file mode 100644 index d15610f11c..0000000000 --- a/cms/djangoapps/contentstore/tests/factories.py +++ /dev/null @@ -1,49 +0,0 @@ -from factory import Factory -from datetime import datetime -from uuid import uuid4 -from student.models import (User, UserProfile, Registration, - CourseEnrollmentAllowed) -from django.contrib.auth.models import Group - - -class UserProfileFactory(Factory): - FACTORY_FOR = UserProfile - - user = None - name = 'Robot Studio' - courseware = 'course.xml' - - -class RegistrationFactory(Factory): - FACTORY_FOR = Registration - - user = None - activation_key = uuid4().hex - - -class UserFactory(Factory): - FACTORY_FOR = User - - username = 'robot' - email = 'robot@edx.org' - password = 'test' - first_name = 'Robot' - last_name = 'Tester' - is_staff = False - is_active = True - is_superuser = False - last_login = datetime.now() - date_joined = datetime.now() - - -class GroupFactory(Factory): - FACTORY_FOR = Group - - name = 'test_group' - - -class CourseEnrollmentAllowedFactory(Factory): - FACTORY_FOR = CourseEnrollmentAllowed - - email = 'test@edx.org' - course_id = 'edX/test/2012_Fall' diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 5fb65fd18e..34003d71a4 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -68,6 +68,10 @@ log = logging.getLogger(__name__) COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] +ADVANCED_COMPONENT_TYPES = ['annotatable','combinedopenended', 'peergrading'] +ADVANCED_COMPONENT_CATEGORY = 'advanced' +ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' + # cdodge: these are categories which should not be parented, they are detached from the hierarchy DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] @@ -281,10 +285,31 @@ def edit_unit(request, location): component_templates = defaultdict(list) + # Check if there are any advanced modules specified in the course policy. These modules + # should be specified as a list of strings, where the strings are the names of the modules + # in ADVANCED_COMPONENT_TYPES that should be enabled for the course. + course_metadata = CourseMetadata.fetch(course.location) + course_advanced_keys = course_metadata.get(ADVANCED_COMPONENT_POLICY_KEY, []) + + # Set component types according to course policy file + component_types = list(COMPONENT_TYPES) + if isinstance(course_advanced_keys, list): + course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES] + if len(course_advanced_keys) > 0: + component_types.append(ADVANCED_COMPONENT_CATEGORY) + else: + log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys)) + templates = modulestore().get_items(Location('i4x', 'edx', 'templates')) for template in templates: - if template.location.category in COMPONENT_TYPES: - component_templates[template.location.category].append(( + category = template.location.category + + if category in course_advanced_keys: + category = ADVANCED_COMPONENT_CATEGORY + + if category in component_types: + #This is a hack to create categories for different xmodules + component_templates[category].append(( template.display_name, template.location.url(), 'markdown' in template.metadata, @@ -1109,6 +1134,7 @@ def module_info(request, module_location): else: return HttpResponseBadRequest() + @login_required @ensure_csrf_cookie def get_course_settings(request, org, course, name): @@ -1124,12 +1150,15 @@ def get_course_settings(request, org, course, name): raise PermissionDenied() course_module = modulestore().get_item(location) - course_details = CourseDetails.fetch(location) return render_to_response('settings.html', { 'context_course': course_module, - 'course_location' : location, - 'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder) + 'course_location': location, + 'details_url': reverse(course_settings_updates, + kwargs={"org": org, + "course": course, + "name": name, + "section": "details"}) }) @login_required diff --git a/cms/envs/test.py b/cms/envs/test.py index 7f39e6818b..abe03edd41 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -27,6 +27,9 @@ STATIC_ROOT = TEST_ROOT / "staticfiles" GITHUB_REPO_ROOT = TEST_ROOT / "data" COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" +# Makes the tests run much faster... +SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead + # TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing STATICFILES_DIRS = [ COMMON_ROOT / "static", diff --git a/cms/static/img/large-advanced-icon.png b/cms/static/img/large-advanced-icon.png new file mode 100644 index 0000000000..c6a19ea5a9 Binary files /dev/null and b/cms/static/img/large-advanced-icon.png differ diff --git a/cms/static/img/large-annotations-icon.png b/cms/static/img/large-annotations-icon.png new file mode 100644 index 0000000000..249193521f Binary files /dev/null and b/cms/static/img/large-annotations-icon.png differ diff --git a/cms/static/img/large-openended-icon.png b/cms/static/img/large-openended-icon.png new file mode 100644 index 0000000000..4d31815413 Binary files /dev/null and b/cms/static/img/large-openended-icon.png differ diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index 97d71f6c79..148df7a325 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -59,11 +59,6 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ // NOTE don't return empty errors as that will be interpreted as an error state }, - url: function() { - var location = this.get('location'); - return '/' + location.get('org') + "/" + location.get('course') + '/settings-details/' + location.get('name') + '/section/details'; - }, - _videokey_illegal_chars : /[^a-zA-Z0-9_-]/g, save_videosource: function(newsource) { // newsource either is