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/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 8e4a016a0f..c0ab9ec60e 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -263,7 +263,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # note, we know the link it should be because that's what in the 'full' course in the test data self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf') + def test_export_course_with_unknown_metadata(self): + ms = modulestore('direct') + cs = contentstore() + import_from_xml(ms, 'common/test/data/', ['full']) + location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') + + root_dir = path(mkdtemp_clean()) + + course = ms.get_item(location) + + # add a bool piece of unknown metadata so we can verify we don't throw an exception + course.metadata['new_metadata'] = True + + ms.update_metadata(location, course.metadata) + + print 'Exporting to tempdir = {0}'.format(root_dir) + + # export out to a tempdir + bExported = False + try: + export_to_xml(ms, cs, location, root_dir, 'test_export') + bExported = True + except Exception: + pass + + self.assertTrue(bExported) class ContentStoreTest(ModuleStoreTestCase): """ 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/dev.py b/cms/envs/dev.py index 3dee93a398..9164c02e3f 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -4,9 +4,6 @@ This config file runs the simplest dev environment""" from .common import * from logsettings import get_logger_config -import logging -import sys - DEBUG = True TEMPLATE_DEBUG = DEBUG LOGGING = get_logger_config(ENV_ROOT / "log", @@ -107,3 +104,36 @@ CACHE_TIMEOUT = 0 # Dummy secret key for dev SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' + +################################ DEBUG TOOLBAR ################################# +INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo') +MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware',) +INTERNAL_IPS = ('127.0.0.1',) + +DEBUG_TOOLBAR_PANELS = ( + 'debug_toolbar.panels.version.VersionDebugPanel', + 'debug_toolbar.panels.timer.TimerDebugPanel', + 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel', + 'debug_toolbar.panels.headers.HeaderDebugPanel', + 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel', + 'debug_toolbar.panels.sql.SQLDebugPanel', + 'debug_toolbar.panels.signals.SignalDebugPanel', + 'debug_toolbar.panels.logger.LoggingPanel', +# This is breaking Mongo updates-- Christina is investigating. +# 'debug_toolbar_mongo.panel.MongoDebugPanel', + + # Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and + # Django=1.3.1/1.4 where requests to views get duplicated (your method gets + # hit twice). So you can uncomment when you need to diagnose performance + # problems, but you shouldn't leave it on. + # 'debug_toolbar.panels.profiling.ProfilingDebugPanel', + ) + +DEBUG_TOOLBAR_CONFIG = { + 'INTERCEPT_REDIRECTS': False +} + +# To see stacktraces for MongoDB queries, set this to True. +# Stacktraces slow down page loads drastically (for pages with lots of queries). +# DEBUG_TOOLBAR_MONGO_STACKTRACES = False 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 or just the "speed:key, *" string diff --git a/cms/static/js/models/settings/course_settings.js b/cms/static/js/models/settings/course_settings.js deleted file mode 100644 index 62b214e853..0000000000 --- a/cms/static/js/models/settings/course_settings.js +++ /dev/null @@ -1,42 +0,0 @@ -if (!CMS.Models['Settings']) CMS.Models.Settings = new Object(); -CMS.Models.Settings.CourseSettings = Backbone.Model.extend({ - // a container for the models representing the n possible tabbed states - defaults: { - courseLocation: null, - details: null, - faculty: null, - grading: null, - problems: null, - discussions: null - }, - - retrieve: function(submodel, callback) { - if (this.get(submodel)) callback(); - else { - var cachethis = this; - switch (submodel) { - case 'details': - var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')}); - details.fetch( { - success : function(model) { - cachethis.set('details', model); - callback(model); - } - }); - break; - case 'grading': - var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')}); - grading.fetch( { - success : function(model) { - cachethis.set('grading', model); - callback(model); - } - }); - break; - - default: - break; - } - } - } -}) \ No newline at end of file diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js index 277a15b57c..8382fb15eb 100644 --- a/cms/static/js/views/course_info_edit.js +++ b/cms/static/js/views/course_info_edit.js @@ -44,6 +44,8 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ self.render(); } ); + // when the client refetches the updates as a whole, re-render them + this.listenTo(this.collection, 'reset', this.render); }, render: function () { @@ -53,8 +55,12 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ $(updateEle).empty(); var self = this; this.collection.each(function (update) { - var newEle = self.template({ updateModel : update }); - $(updateEle).append(newEle); + try { + var newEle = self.template({ updateModel : update }); + $(updateEle).append(newEle); + } catch (e) { + // ignore + } }); this.$el.find(".new-update-form").hide(); this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' }); @@ -150,7 +156,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ }, closeEditor: function(self, removePost) { - var targetModel = self.collection.getByCid(self.$currentPost.attr('name')); + var targetModel = self.collection.get(self.$currentPost.attr('name')); if(removePost) { self.$currentPost.remove(); @@ -160,8 +166,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ self.$currentPost.removeClass('editing'); self.$currentPost.find('.date-display').html(targetModel.get('date')); self.$currentPost.find('.date').val(targetModel.get('date')); - self.$currentPost.find('.update-contents').html(targetModel.get('content')); - self.$currentPost.find('.new-update-content').val(targetModel.get('content')); + try { + // just in case the content causes an error (embedded js errors) + self.$currentPost.find('.update-contents').html(targetModel.get('content')); + self.$currentPost.find('.new-update-content').val(targetModel.get('content')); + } catch (e) { + // ignore but handle rest of page + } self.$currentPost.find('form').hide(); window.$modalCover.unbind('click'); window.$modalCover.hide(); @@ -172,7 +183,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ // Dereferencing from events to screen elements eventModel: function(event) { // not sure if it should be currentTarget or delegateTarget - return this.collection.getByCid($(event.currentTarget).attr("name")); + return this.collection.get($(event.currentTarget).attr("name")); }, modelDom: function(event) { diff --git a/cms/static/sass/_graphics.scss b/cms/static/sass/_graphics.scss index 4ed22c570d..300cf3b692 100644 --- a/cms/static/sass/_graphics.scss +++ b/cms/static/sass/_graphics.scss @@ -254,6 +254,30 @@ background: url(../img/html-icon.png) center no-repeat; } +.large-openended-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-openended-icon.png) center no-repeat; +} + +.large-annotations-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-annotations-icon.png) center no-repeat; +} + +.large-advanced-icon { + display: inline-block; + width: 100px; + height: 60px; + margin-right: 5px; + background: url(../img/large-advanced-icon.png) center no-repeat; +} + .large-textbook-icon { display: inline-block; width: 100px; diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index 5ace98df56..a5a9144b07 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -28,7 +28,7 @@ {{uploadDate}}