diff --git a/.gitignore b/.gitignore
index b13a128a63..8fb170c30f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,4 +29,5 @@ cover_html/
.idea/
.redcar/
chromedriver.log
+/nbproject
ghostdriver.log
diff --git a/.pylintrc b/.pylintrc
index 4357885133..6690bb7df0 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -33,7 +33,15 @@ load-plugins=
# can either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
-disable=E1102,W0142
+disable=
+# W0141: Used builtin function 'map'
+# W0142: Used * or ** magic
+# R0201: Method could be a function
+# R0901: Too many ancestors
+# R0902: Too many instance attributes
+# R0903: Too few public methods (1/2)
+# R0904: Too many public methods
+ W0141,W0142,R0201,R0901,R0902,R0903,R0904
[REPORTS]
@@ -97,7 +105,7 @@ bad-functions=map,filter,apply,input
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names
-const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log)$
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$
# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
@@ -106,7 +114,7 @@ class-rgx=[A-Z_][a-zA-Z0-9]+$
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct method names
-method-rgx=[a-z_][a-z0-9_]{2,30}$
+method-rgx=([a-z_][a-z0-9_]{2,60}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*)$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py
index 153d13dd13..8c8aed549d 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
@@ -26,9 +26,9 @@ def get_course_updates(location):
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = html.fromstring(course_updates.definition['data'])
- except:
- course_html_parsed = html.fromstring("
")
+ course_html_parsed = etree.fromstring(course_updates.data)
+ except etree.XMLSyntaxError:
+ course_html_parsed = etree.fromstring("")
# Confirm that root is , iterate over
, pull out
subs and then rest of val
course_upd_collection = []
@@ -60,13 +60,13 @@ def update_course_updates(location, update, passed_id=None):
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
- return HttpResponseBadRequest
+ return HttpResponseBadRequest()
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = html.fromstring(course_updates.definition['data'])
- except:
- course_html_parsed = html.fromstring("")
+ course_html_parsed = etree.fromstring(course_updates.data)
+ except etree.XMLSyntaxError:
+ course_html_parsed = etree.fromstring("")
# No try/catch b/c failure generates an error back to client
new_html_parsed = html.fromstring('
' + update['date'] + '
' + update['content'] + '
')
@@ -85,13 +85,12 @@ def update_course_updates(location, update, passed_id=None):
passed_id = course_updates.location.url() + "/" + str(idx)
# update db record
- course_updates.definition['data'] = html.tostring(course_html_parsed)
- modulestore('direct').update_item(location, course_updates.definition['data'])
-
- return {"id": passed_id,
- "date": update['date'],
- "content": update['content']}
+ course_updates.data = etree.tostring(course_html_parsed)
+ modulestore('direct').update_item(location, course_updates.data)
+ return {"id" : passed_id,
+ "date" : update['date'],
+ "content" :update['content']}
def delete_course_update(location, update, passed_id):
"""
@@ -99,19 +98,19 @@ def delete_course_update(location, update, passed_id):
Returns the resulting course_updates b/c their ids change.
"""
if not passed_id:
- return HttpResponseBadRequest
+ return HttpResponseBadRequest()
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
- return HttpResponseBadRequest
+ return HttpResponseBadRequest()
# TODO use delete_blank_text parser throughout and cache as a static var in a class
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
- course_html_parsed = html.fromstring(course_updates.definition['data'])
- except:
- course_html_parsed = html.fromstring("")
+ course_html_parsed = etree.fromstring(course_updates.data)
+ except etree.XMLSyntaxError:
+ course_html_parsed = etree.fromstring("")
if course_html_parsed.tag == 'ol':
# ??? Should this use the id in the json or in the url or does it matter?
@@ -122,9 +121,9 @@ def delete_course_update(location, update, passed_id):
course_html_parsed.remove(element_to_delete)
# update db record
- course_updates.definition['data'] = html.tostring(course_html_parsed)
+ course_updates.data = etree.tostring(course_html_parsed)
store = modulestore('direct')
- store.update_item(location, course_updates.definition['data'])
+ store.update_item(location, course_updates.data)
return get_course_updates(location)
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature
index 779d44e4b2..af97709ad0 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.feature
+++ b/cms/djangoapps/contentstore/features/advanced-settings.feature
@@ -2,53 +2,41 @@ Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key/value pairs
- Scenario: A course author sees only display_name on a newly created course
+ Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio
When I select the Advanced Settings
- Then I see only the display name
+ Then I see default advanced settings
- @skip-phantom
- Scenario: Test if there are no policy settings without existing UI controls
+ Scenario: Add new entries, and they appear alphabetically after save
Given I am on the Advanced Course Settings page in Studio
- When I delete the display name
- Then there are no advanced policy settings
- And I reload the page
- Then there are no advanced policy settings
-
- @skip-phantom
- Scenario: Test cancel editing key name
- Given I am on the Advanced Course Settings page in Studio
- When I edit the name of a policy key
- And I press the "Cancel" notification button
- Then the policy key name is unchanged
-
- Scenario: Test editing key name
- Given I am on the Advanced Course Settings page in Studio
- When I edit the name of a policy key
- And I press the "Save" notification button
- Then the policy key name is changed
+ Then the settings are alphabetized
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Cancel" notification button
Then the policy key value is unchanged
+ And I reload the page
+ Then the policy key value is unchanged
- @skip-phantom
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Save" notification button
Then the policy key value is changed
-
- Scenario: Add new entries, and they appear alphabetically after save
- Given I am on the Advanced Course Settings page in Studio
- When I create New Entries
- Then they are alphabetized
And I reload the page
- Then they are alphabetized
+ Then the policy key value is changed
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
- When I create a JSON object
+ When I create a JSON object as a value
Then it is displayed as formatted
+ And I reload the page
+ Then it is displayed as formatted
+
+ Scenario: Test automatic quoting of non-JSON values
+ Given I am on the Advanced Course Settings page in Studio
+ When I create a non-JSON value not in quotes
+ Then it is displayed as a string
+ And I reload the page
+ Then it is displayed as a string
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py
index 1024579b45..7e86e94a31 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.py
+++ b/cms/djangoapps/contentstore/features/advanced-settings.py
@@ -1,6 +1,7 @@
from lettuce import world, step
from common import *
import time
+from terrain.steps import reload_the_page
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.support import expected_conditions as EC
@@ -11,6 +12,10 @@ http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdrive
"""
from selenium.webdriver.common.keys import Keys
+KEY_CSS = '.key input.policy-key'
+VALUE_CSS = 'textarea.json'
+DISPLAY_NAME_KEY = "display_name"
+DISPLAY_NAME_VALUE = '"Robot Super Course"'
############### ACTIONS ####################
@step('I select the Advanced Settings$')
@@ -20,7 +25,6 @@ def i_select_advanced_settings(step):
css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a'
css_click(link_css)
- # world.browser.click_link_by_text('Advanced Settings')
@step('I am on the Advanced Course Settings page in Studio$')
@@ -29,35 +33,27 @@ def i_am_on_advanced_course_settings(step):
step.given('I select the Advanced Settings')
-# TODO: this is copied from terrain's step.py. Need to figure out how to share that code.
-@step('I reload the page$')
-def reload_the_page(step):
- world.browser.reload()
-
-
-@step(u'I edit the name of a policy key$')
-def edit_the_name_of_a_policy_key(step):
- policy_key_css = 'input.policy-key'
- e = css_find(policy_key_css).first
- e.type('_new')
-
-
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
def is_visible(driver):
- return EC.visibility_of_element_located((By.CSS_SELECTOR,css,))
- def is_invisible(driver):
- return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
+ return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
+
+ # def is_invisible(driver):
+ # return EC.invisibility_of_element_located((By.CSS_SELECTOR,css,))
css = 'a.%s-button' % name.lower()
wait_for(is_visible)
+ time.sleep(float(1))
+ css_click_at(css)
+
+# is_invisible is not returning a boolean, not working
+# try:
+# css_click_at(css)
+# wait_for(is_invisible)
+# except WebDriverException, e:
+# css_click_at(css)
+# wait_for(is_invisible)
- try:
- css_click_at(css)
- wait_for(is_invisible)
- except WebDriverException, e:
- css_click_at(css)
- wait_for(is_invisible)
@step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step):
@@ -65,133 +61,86 @@ def edit_the_value_of_a_policy_key(step):
It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :)
"""
- policy_key_css = 'input.policy-key'
- e = css_find(policy_key_css).first
+ e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
-@step('I delete the display name$')
-def delete_the_display_name(step):
- delete_entry(0)
- click_save()
-
-
-@step('create New Entries$')
-def create_new_entries(step):
- create_entry("z", "apple")
- create_entry("a", "zebra")
- click_save()
-
-
-@step('I create a JSON object$')
+@step('I create a JSON object as a value$')
def create_JSON_object(step):
- create_entry("json", '{"key": "value", "key_2": "value_2"}')
- click_save()
+ change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
+
+
+@step('I create a non-JSON value not in quotes$')
+def create_value_not_in_quotes(step):
+ change_display_name_value(step, 'quote me')
############### RESULTS ####################
-@step('I see only the display name$')
-def i_see_only_display_name(step):
- assert_policy_entries(["display_name"], ['"Robot Super Course"'])
+@step('I see default advanced settings$')
+def i_see_default_advanced_settings(step):
+ # Test only a few of the existing properties (there are around 34 of them)
+ assert_policy_entries(
+ ["advanced_modules", DISPLAY_NAME_KEY, "show_calculator"], ["[]", DISPLAY_NAME_VALUE, "false"])
-@step('there are no advanced policy settings$')
-def no_policy_settings(step):
- keys_css = 'input.policy-key'
- val_css = 'textarea.json'
- k = world.browser.is_element_not_present_by_css(keys_css, 5)
- v = world.browser.is_element_not_present_by_css(val_css, 5)
- assert_true(k)
- assert_true(v)
-
-
-@step('they are alphabetized$')
+@step('the settings are alphabetized$')
def they_are_alphabetized(step):
- assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"'])
+ key_elements = css_find(KEY_CSS)
+ all_keys = []
+ for key in key_elements:
+ all_keys.append(key.value)
+
+ assert_equal(sorted(all_keys), all_keys, "policy keys were not sorted")
@step('it is displayed as formatted$')
def it_is_formatted(step):
- assert_policy_entries(["display_name", "json"], ['"Robot Super Course"', '{\n "key": "value",\n "key_2": "value_2"\n}'])
+ assert_policy_entries([DISPLAY_NAME_KEY], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
-@step(u'the policy key name is unchanged$')
-def the_policy_key_name_is_unchanged(step):
- policy_key_css = 'input.policy-key'
- val = css_find(policy_key_css).first.value
- assert_equal(val, 'display_name')
-
-
-@step(u'the policy key name is changed$')
-def the_policy_key_name_is_changed(step):
- policy_key_css = 'input.policy-key'
- val = css_find(policy_key_css).first.value
- assert_equal(val, 'display_name_new')
+@step('it is displayed as a string')
+def it_is_formatted(step):
+ assert_policy_entries([DISPLAY_NAME_KEY], ['"quote me"'])
@step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step):
- policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
- val = css_find(policy_value_css).first.value
- assert_equal(val, '"Robot Super Course"')
+ assert_equal(get_display_name_value(), DISPLAY_NAME_VALUE)
@step(u'the policy key value is changed$')
-def the_policy_key_value_is_unchanged(step):
- policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
- val = css_find(policy_value_css).first.value
- assert_equal(val, '"Robot Super Course X"')
+def the_policy_key_value_is_changed(step):
+ assert_equal(get_display_name_value(), '"Robot Super Course X"')
############# HELPERS ###############
-def create_entry(key, value):
- # Scroll down the page so the button is visible
- world.scroll_to_bottom()
- css_click_at('a.new-advanced-policy-item', 10, 10)
- new_key_css = 'div#__new_advanced_key__ input'
- new_key_element = css_find(new_key_css).first
- new_key_element.fill(key)
-# For some reason have to get the instance for each command
-# (get error that it is no longer attached to the DOM)
-# Have to do all this because Selenium fill does not remove existing text
- new_value_css = 'div.CodeMirror textarea'
- css_find(new_value_css).last.fill("")
- css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE)
- css_find(new_value_css).last.fill(value)
- # Add in a TAB key press because intermittently on ubuntu the
- # last character of "value" above was not getting typed in
- css_find(new_value_css).last._element.send_keys(Keys.TAB)
-
-
-def delete_entry(index):
- """
- Delete the nth entry where index is 0-based
- """
- css = 'a.delete-button'
- assert_true(world.browser.is_element_present_by_css(css, 5))
- delete_buttons = css_find(css)
- assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index))
- delete_buttons[index].click()
-
-
def assert_policy_entries(expected_keys, expected_values):
- assert_entries('.key input.policy-key', expected_keys)
- assert_entries('textarea.json', expected_values)
+ for counter in range(len(expected_keys)):
+ index = get_index_of(expected_keys[counter])
+ assert_false(index == -1, "Could not find key: " + expected_keys[counter])
+ assert_equal(expected_values[counter], css_find(VALUE_CSS)[index].value, "value is incorrect")
-def assert_entries(css, expected_values):
- webElements = css_find(css)
- assert_equal(len(expected_values), len(webElements))
-# Sometimes get stale reference if I hold on to the array of elements
- for counter in range(len(expected_values)):
- assert_equal(expected_values[counter], css_find(css)[counter].value)
+def get_index_of(expected_key):
+ for counter in range(len(css_find(KEY_CSS))):
+ # Sometimes get stale reference if I hold on to the array of elements
+ key = css_find(KEY_CSS)[counter].value
+ if key == expected_key:
+ return counter
+
+ return -1
-def click_save():
- css = "a.save-button"
- css_click_at(css)
+def get_display_name_value():
+ index = get_index_of(DISPLAY_NAME_KEY)
+ return css_find(VALUE_CSS)[index].value
-def fill_last_field(value):
- newValue = css_find('#__new_advanced_key__ input').first
- newValue.fill(value)
+def change_display_name_value(step, new_value):
+ e = css_find(KEY_CSS)[get_index_of(DISPLAY_NAME_KEY)]
+ display_name = get_display_name_value()
+ for count in range(len(display_name)):
+ e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE)
+ # Must delete "" before typing the JSON value
+ e._element.send_keys(Keys.TAB, Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
+ press_the_notification_button(step, "Save")
\ No newline at end of file
diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py
index 789226db1a..fc92205030 100644
--- a/cms/djangoapps/contentstore/management/commands/delete_course.py
+++ b/cms/djangoapps/contentstore/management/commands/delete_course.py
@@ -7,7 +7,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import Location
from xmodule.course_module import CourseDescriptor
-from prompt import query_yes_no
+from .prompt import query_yes_no
from auth.authz import _delete_course_group
diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py
index 7ed4505c94..8ea6add88d 100644
--- a/cms/djangoapps/contentstore/module_info_model.py
+++ b/cms/djangoapps/contentstore/module_info_model.py
@@ -15,10 +15,10 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
- data = module.definition['data']
+ data = module.data
if rewrite_static_links:
data = replace_static_urls(
- module.definition['data'],
+ module.data,
None,
course_namespace=Location([
module.location.tag,
@@ -32,7 +32,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
return {
'id': module.location.url(),
'data': data,
- 'metadata': module.metadata
+ # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
+ 'metadata': module._model_data._kvs._metadata
}
@@ -70,23 +71,23 @@ def set_module_info(store, location, post_data):
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata']
-
+
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
- for metadata_key in posted_metadata.keys():
-
+ for metadata_key, value in posted_metadata.items():
+
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
- if metadata_key in module.metadata:
- del module.metadata[metadata_key]
+ if metadata_key in module._model_data:
+ del module._model_data[metadata_key]
del posted_metadata[metadata_key]
-
- # overlay the new metadata over the modulestore sourced collection to support partial updates
- module.metadata.update(posted_metadata)
-
+ else:
+ module._model_data[metadata_key] = value
+
# commit to datastore
- store.update_metadata(location, module.metadata)
+ # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
+ store.update_metadata(location, module._model_data._kvs._metadata)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 99ef1169b1..d04e1a6332 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -6,15 +6,16 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
from tempdir import mkdtemp_clean
+from datetime import timedelta
import json
from fs.osfs import OSFS
import copy
from json import loads
from django.contrib.auth.models import User
-from cms.djangoapps.contentstore.utils import get_modulestore
+from contentstore.utils import get_modulestore
-from utils import ModuleStoreTestCase, parse_json
+from .utils import ModuleStoreTestCase, parse_json
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location
@@ -25,6 +26,7 @@ from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
+from xmodule.modulestore.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
@@ -109,10 +111,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
- self.assertTrue(sequential.location.url() in chapter.definition['children'])
+ self.assertTrue(sequential.location.url() in chapter.children)
- self.client.post(reverse('delete_item'),
- json.dumps({'id': sequential.location.url(), 'delete_children':'true'}),
+ self.client.post(reverse('delete_item'),
+ json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
"application/json")
found = False
@@ -127,9 +129,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
- self.assertFalse(sequential.location.url() in chapter.definition['children'])
+ self.assertFalse(sequential.location.url() in chapter.children)
+
-
def test_about_overrides(self):
'''
@@ -139,11 +141,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
- self.assertEqual(effort.definition['data'], '6 hours')
+ self.assertEqual(effort.data, '6 hours')
# this one should be in a non-override folder
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
- self.assertEqual(effort.definition['data'], 'TBD')
+ self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -153,7 +155,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location)
- self.assertNotIn('hide_progress_tab', course.metadata)
+ self.assertFalse(course.hide_progress_tab)
def test_clone_course(self):
@@ -246,7 +248,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json', 'r') as grading_policy:
on_disk = loads(grading_policy.read())
- self.assertEqual(on_disk, course.definition['data']['grading_policy'])
+ self.assertEqual(on_disk, course.grading_policy)
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
@@ -255,7 +257,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
with fs.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
- self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
+ self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
# remove old course
delete_course(module_store, content_store, location)
@@ -302,10 +304,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
course = module_store.get_item(location)
+ metadata = own_metadata(course)
# add a bool piece of unknown metadata so we can verify we don't throw an exception
- course.metadata['new_metadata'] = True
+ metadata['new_metadata'] = True
- module_store.update_metadata(location, course.metadata)
+ module_store.update_metadata(location, metadata)
print 'Exporting to tempdir = {0}'.format(root_dir)
@@ -473,21 +476,20 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
context = problem.get_context()
self.assertIn('markdown', context, "markdown is missing from context")
- self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
module_store = modulestore('direct')
did_load_item = False
- try:
+ try:
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
did_load_item = True
except ItemNotFoundError:
pass
# make sure we found the item (e.g. it didn't error while loading)
- self.assertTrue(did_load_item)
+ self.assertTrue(did_load_item)
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -499,8 +501,7 @@ class ContentStoreTest(ModuleStoreTestCase):
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
- self.assertIn('xqa_key', vertical.metadata)
- self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
+ self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
self.assertGreater(len(verticals), 0)
@@ -510,36 +511,33 @@ class ContentStoreTest(ModuleStoreTestCase):
# crate a new module and add it as a child to a vertical
module_store.clone_item(source_template_location, new_component_location)
parent = verticals[0]
- module_store.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
+ module_store.update_children(parent.location, parent.children + [new_component_location.url()])
# flush the cache
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level
- self.assertIn('graceperiod', new_module.metadata)
+ self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
- self.assertEqual(parent.metadata['graceperiod'], new_module.metadata['graceperiod'])
-
- self.assertEqual(course.metadata['xqa_key'], new_module.metadata['xqa_key'])
+ self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)
#
# now let's define an override at the leaf node level
#
- new_module.metadata['graceperiod'] = '1 day'
- module_store.update_metadata(new_module.location, new_module.metadata)
+ new_module.lms.graceperiod = timedelta(1)
+ module_store.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch
module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = module_store.get_item(new_component_location)
- self.assertIn('graceperiod', new_module.metadata)
- self.assertEqual('1 day', new_module.metadata['graceperiod'])
+ self.assertEqual(timedelta(1), new_module.lms.graceperiod)
class TemplateTestCase(ModuleStoreTestCase):
- def test_template_cleanup(self):
+ def test_template_cleanup(self):
module_store = modulestore('direct')
# insert a bogus template in the store
@@ -562,4 +560,3 @@ class TemplateTestCase(ModuleStoreTestCase):
asserted = True
self.assertTrue(asserted)
-
diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index 5560d2e39b..ecdeca29e7 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -10,16 +10,16 @@ from django.core.urlresolvers import reverse
from django.utils.timezone import UTC
from xmodule.modulestore import Location
-from cms.djangoapps.models.settings.course_details import (CourseDetails,
+from models.settings.course_details import (CourseDetails,
CourseSettingsEncoder)
-from cms.djangoapps.models.settings.course_grading import CourseGradingModel
-from cms.djangoapps.contentstore.utils import get_modulestore
+from models.settings.course_grading import CourseGradingModel
+from contentstore.utils import get_modulestore
from django.test import TestCase
-from utils import ModuleStoreTestCase
+from .utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
-from cms.djangoapps.models.settings.course_metadata import CourseMetadata
+from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
@@ -246,8 +246,9 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
- test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0}
+ test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
+ print test_grader.grace_period, altered_grader.grace_period
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
def test_update_grader_from_json(self):
@@ -286,31 +287,31 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course_location,
- { "a" : 1,
- "b_a_c_h" : { "c" : "test" },
- "test_text" : "a text string"})
+ { "advertised_start" : "start A",
+ "testcenter_info" : { "c" : "test" },
+ "days_early_for_beta" : 2})
self.update_check(test_model)
# try fresh fetch to ensure persistence
test_model = CourseMetadata.fetch(self.course_location)
self.update_check(test_model)
# now change some of the existing metadata
test_model = CourseMetadata.update_from_json(self.course_location,
- { "a" : 2,
+ { "advertised_start" : "start B",
"display_name" : "jolly roger"})
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
- self.assertIn('a', test_model, 'Missing revised a metadata field')
- self.assertEqual(test_model['a'], 2, "a not expected value")
+ self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
+ self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value")
def update_check(self, test_model):
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
- self.assertIn('a', test_model, 'Missing new a metadata field')
- self.assertEqual(test_model['a'], 1, "a not expected value")
- self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field')
- self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value")
- self.assertIn('test_text', test_model, 'Missing test_text metadata field')
- self.assertEqual(test_model['test_text'], "a text string", "test_text not expected value")
+ self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
+ self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
+ self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
+ self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
+ self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
+ self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
def test_delete_key(self):
@@ -321,5 +322,5 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness
- self.assertNotIn('showanswer', test_model, 'showanswer field still in')
- self.assertNotIn('xqa_key', test_model, 'xqa_key field still in')
\ No newline at end of file
+ self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
+ self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py
index c57f1322f5..6a3a1e21f7 100644
--- a/cms/djangoapps/contentstore/tests/test_course_updates.py
+++ b/cms/djangoapps/contentstore/tests/test_course_updates.py
@@ -1,4 +1,4 @@
-from cms.djangoapps.contentstore.tests.test_course_settings import CourseTestCase
+from contentstore.tests.test_course_settings import CourseTestCase
from django.core.urlresolvers import reverse
import json
diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py
index 09e3b045f9..4ab40d17a8 100644
--- a/cms/djangoapps/contentstore/tests/test_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_utils.py
@@ -1,4 +1,4 @@
-from cms.djangoapps.contentstore import utils
+from contentstore import utils
import mock
from django.test import TestCase
diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py
index c4a46459e2..e43a95fccd 100644
--- a/cms/djangoapps/contentstore/tests/tests.py
+++ b/cms/djangoapps/contentstore/tests/tests.py
@@ -8,7 +8,7 @@ import json
from fs.osfs import OSFS
import copy
-from cms.djangoapps.contentstore.utils import get_modulestore
+from contentstore.utils import get_modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
@@ -24,7 +24,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
-from utils import ModuleStoreTestCase, parse_json, user, registration
+from .utils import ModuleStoreTestCase, parse_json, user, registration
class ContentStoreTestCase(ModuleStoreTestCase):
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index cba30131b5..0a99441fe9 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -39,10 +39,10 @@ def get_course_location_for_item(location):
# make sure we found exactly one match on this above course search
found_cnt = len(courses)
if found_cnt == 0:
- raise BaseException('Could not find course at {0}'.format(course_search_location))
+ raise Exception('Could not find course at {0}'.format(course_search_location))
if found_cnt > 1:
- raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
+ raise Exception('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses))
location = courses[0].location
@@ -136,7 +136,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS
"""
- if unit.metadata.get('is_draft', False):
+ if unit.cms.is_draft:
try:
modulestore('direct').get_item(unit.location)
return UnitState.draft
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index f57299596c..8809e19acf 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -28,11 +28,15 @@ from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
+from xmodule.modulestore.inheritance import own_metadata
+from xblock.core import Scope
+from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
import static_replace
from external_auth.views import ssl_login_shortcut
+from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
@@ -54,12 +58,12 @@ from contentstore.course_info_model import get_course_updates,\
from cache_toolbox.core import del_cached_content
from xmodule.timeparse import stringify_time
from contentstore.module_info_model import get_module_info, set_module_info
-from cms.djangoapps.models.settings.course_details import CourseDetails,\
+from models.settings.course_details import CourseDetails,\
CourseSettingsEncoder
-from cms.djangoapps.models.settings.course_grading import CourseGradingModel
-from cms.djangoapps.contentstore.utils import get_modulestore
+from models.settings.course_grading import CourseGradingModel
+from contentstore.utils import get_modulestore
from django.shortcuts import redirect
-from cms.djangoapps.models.settings.course_metadata import CourseMetadata
+from models.settings.course_metadata import CourseMetadata
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
@@ -110,7 +114,7 @@ def login_page(request):
def howitworks(request):
if request.user.is_authenticated():
return index(request)
- else:
+ else:
return render_to_response('howitworks.html', {})
def ux_alerts(request):
@@ -138,7 +142,7 @@ def index(request):
return render_to_response('index.html', {
'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
- 'courses': [(course.metadata.get('display_name'),
+ 'courses': [(course.display_name,
reverse('course_index', args=[
course.location.org,
course.location.course,
@@ -242,8 +246,13 @@ def edit_subsection(request, location):
# remove all metadata from the generic dictionary that is presented in a more normalized UI
- policy_metadata = dict((key, value) for key, value in item.metadata.iteritems()
- if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
+ policy_metadata = dict(
+ (field.name, field.read_from(item))
+ for field
+ in item.fields
+ if field.name not in ['display_name', 'start', 'due', 'format'] and
+ field.scope == Scope.settings
+ )
can_view_live = False
subsection_units = item.get_children()
@@ -296,8 +305,7 @@ def edit_unit(request, location):
# 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, [])
+ course_advanced_keys = course.advanced_modules
# Set component types according to course policy file
component_types = list(COMPONENT_TYPES)
@@ -318,10 +326,10 @@ def edit_unit(request, location):
if category in component_types:
#This is a hack to create categories for different xmodules
component_templates[category].append((
- template.display_name,
+ template.display_name_with_default,
template.location.url(),
- 'markdown' in template.metadata,
- 'empty' in template.metadata
+ hasattr(template, 'markdown') and template.markdown is not None,
+ template.cms.empty,
))
components = [
@@ -365,11 +373,6 @@ def edit_unit(request, location):
unit_state = compute_unit_state(item)
- try:
- published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date'))
- except TypeError:
- published_date = None
-
return render_to_response('unit.html', {
'context_course': course,
'active_tab': 'courseware',
@@ -380,11 +383,11 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
- 'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else None,
+ 'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
- 'published_date': published_date,
+ 'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
})
@@ -449,9 +452,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
dispatch: The action to execute
"""
- instance_state, shared_state = load_preview_state(request, preview_id, location)
descriptor = modulestore().get_item(location)
- instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
+ instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
@@ -462,46 +464,9 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
log.exception("error processing ajax call")
raise
- save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
return HttpResponse(ajax_return)
-def load_preview_state(request, preview_id, location):
- """
- Load the state of a preview module from the request
-
- preview_id (str): An identifier specifying which preview this module is used for
- location: The Location of the module to dispatch to
- """
- if 'preview_states' not in request.session:
- request.session['preview_states'] = defaultdict(dict)
-
- instance_state = request.session['preview_states'][preview_id, location].get('instance')
- shared_state = request.session['preview_states'][preview_id, location].get('shared')
-
- return instance_state, shared_state
-
-
-def save_preview_state(request, preview_id, location, instance_state, shared_state):
- """
- Save the state of a preview module to the request
-
- preview_id (str): An identifier specifying which preview this module is used for
- location: The Location of the module to dispatch to
- instance_state: The instance state to save
- shared_state: The shared state to save
- """
- if 'preview_states' not in request.session:
- request.session['preview_states'] = defaultdict(dict)
-
- # request.session doesn't notice indirect changes; so, must set its dict w/ every change to get
- # it to persist: http://www.djangobook.com/en/2.0/chapter14.html
- preview_states = request.session['preview_states']
- preview_states[preview_id, location]['instance'] = instance_state
- preview_states[preview_id, location]['shared'] = shared_state
- request.session['preview_states'] = preview_states # make session mgmt notice the update
-
-
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
@@ -509,6 +474,33 @@ def render_from_lms(template_name, dictionary, context=None, namespace='main'):
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
+class SessionKeyValueStore(KeyValueStore):
+ def __init__(self, request, model_data):
+ self._model_data = model_data
+ self._session = request.session
+
+ def get(self, key):
+ try:
+ return self._model_data[key.field_name]
+ except (KeyError, InvalidScopeError):
+ return self._session[tuple(key)]
+
+ def set(self, key, value):
+ try:
+ self._model_data[key.field_name] = value
+ except (KeyError, InvalidScopeError):
+ self._session[tuple(key)] = value
+
+ def delete(self, key):
+ try:
+ del self._model_data[key.field_name]
+ except (KeyError, InvalidScopeError):
+ del self._session[tuple(key)]
+
+ def has(self, key):
+ return key in self._model_data or key in self._session
+
+
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
@@ -519,6 +511,14 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor
"""
+ def preview_model_data(descriptor):
+ return DbModel(
+ SessionKeyValueStore(request, descriptor._model_data),
+ descriptor.module_class,
+ preview_id,
+ MongoUsage(preview_id, descriptor.location.url()),
+ )
+
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
@@ -529,6 +529,7 @@ def preview_module_system(request, preview_id, descriptor):
debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
user=request.user,
+ xblock_model_data=preview_model_data,
)
@@ -541,11 +542,11 @@ def get_preview_module(request, preview_id, descriptor):
preview_id (str): An identifier specifying which preview this module is used for
location: A Location
"""
- instance_state, shared_state = descriptor.get_sample_state()[0]
- return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
+
+ return load_preview_module(request, preview_id, descriptor)
-def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
+def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
@@ -557,12 +558,13 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
"""
system = preview_module_system(request, preview_id, descriptor)
try:
- module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
+ module = descriptor.xmodule(system)
except:
+ log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
- ).xmodule_constructor(system)(None, None)
+ ).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
@@ -580,11 +582,9 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module.get_html = replace_static_urls(
module.get_html,
- module.metadata.get('data_dir', module.location.course),
+ getattr(module, 'data_dir', module.location.course),
course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
)
- save_preview_state(request, preview_id, descriptor.location.url(),
- module.get_instance_state(), module.get_shared_state())
return module
@@ -598,7 +598,7 @@ def get_module_previews(request, descriptor):
"""
preview_html = []
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
- module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
+ module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
@@ -646,15 +646,17 @@ def delete_item(request):
modulestore('direct').delete_item(item.location)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
+ if delete_all_versions:
+ parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
- parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
-
- for parent_loc in parent_locs:
- parent = modulestore('direct').get_item(parent_loc)
- item_url = item_loc.url()
- if item_url in parent.definition["children"]:
- parent.definition["children"].remove(item_url)
- modulestore('direct').update_children(parent.location, parent.definition["children"])
+ for parent_loc in parent_locs:
+ parent = modulestore('direct').get_item(parent_loc)
+ item_url = item_loc.url()
+ if item_url in parent.children:
+ children = parent.children
+ children.remove(item_url)
+ parent.children = children
+ modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse()
@@ -693,7 +695,7 @@ def save_item(request):
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
- for metadata_key in posted_metadata.keys():
+ for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
@@ -701,15 +703,15 @@ def save_item(request):
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
- if metadata_key in existing_item.metadata:
- del existing_item.metadata[metadata_key]
+ if metadata_key in existing_item._model_data:
+ del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
-
- # overlay the new metadata over the modulestore sourced collection to support partial updates
- existing_item.metadata.update(posted_metadata)
+ else:
+ existing_item._model_data[metadata_key] = value
# commit to datastore
- store.update_metadata(item_location, existing_item.metadata)
+ # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
+ store.update_metadata(item_location, own_metadata(existing_item))
return HttpResponse()
@@ -776,17 +778,14 @@ def clone_item(request):
new_item = get_modulestore(template).clone_item(template, dest_location)
- # TODO: This needs to be deleted when we have proper storage for static content
- new_item.metadata['data_dir'] = parent.metadata['data_dir']
-
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
- new_item.metadata['display_name'] = display_name
+ new_item.display_name = display_name
- get_modulestore(template).update_metadata(new_item.location.url(), new_item.own_metadata)
+ get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
- get_modulestore(parent.location).update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
+ get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
@@ -1005,7 +1004,7 @@ def reorder_static_tabs(request):
for tab in course.tabs:
if tab['type'] == 'static_tab':
reordered_tabs.append({'type': 'static_tab',
- 'name': tab_items[static_tab_idx].metadata.get('display_name'),
+ 'name': tab_items[static_tab_idx].display_name,
'url_slug': tab_items[static_tab_idx].location.name})
static_tab_idx += 1
else:
@@ -1014,7 +1013,7 @@ def reorder_static_tabs(request):
# OK, re-assemble the static tabs in the new order
course.tabs = reordered_tabs
- modulestore('direct').update_metadata(course.location, course.metadata)
+ modulestore('direct').update_metadata(course.location, own_metadata(course))
return HttpResponse()
@@ -1232,7 +1231,6 @@ def course_config_advanced_page(request, org, course, name):
return render_to_response('settings_advanced.html', {
'context_course': course_module,
'course_location' : location,
- 'advanced_blacklist' : json.dumps(CourseMetadata.FILTERED_LIST),
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
})
@@ -1315,7 +1313,7 @@ def course_advanced_updates(request, org, course, name):
org, course: Attributes of the Location for the item to edit
"""
location = ['i4x', org, course, 'course', name]
-
+
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
@@ -1325,7 +1323,7 @@ def course_advanced_updates(request, org, course, name):
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
-
+
if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE':
@@ -1436,13 +1434,10 @@ def create_new_course(request):
new_course = modulestore('direct').clone_item(template, dest_location)
if display_name is not None:
- new_course.metadata['display_name'] = display_name
-
- # we need a 'data_dir' for legacy reasons
- new_course.metadata['data_dir'] = uuid4().hex
+ new_course.display_name = display_name
# set a default start date to now
- new_course.metadata['start'] = stringify_time(time.gmtime())
+ new_course.start = time.gmtime()
initialize_course_tabs(new_course)
@@ -1461,12 +1456,12 @@ def initialize_course_tabs(course):
# This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there.
course.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
+ {"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
- modulestore('direct').update_metadata(course.location.url(), course.own_metadata)
+ modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@ensure_csrf_cookie
diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py
index b27f4e3804..d3cd5fe164 100644
--- a/cms/djangoapps/models/settings/course_details.py
+++ b/cms/djangoapps/models/settings/course_details.py
@@ -1,13 +1,14 @@
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
+from xmodule.modulestore.inheritance import own_metadata
import json
from json.encoder import JSONEncoder
import time
from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date
-from cms.djangoapps.models.settings import course_grading
-from cms.djangoapps.contentstore.utils import update_item
+from models.settings import course_grading
+from contentstore.utils import update_item
import re
import logging
@@ -43,25 +44,25 @@ class CourseDetails(object):
temploc = course_location._replace(category='about', name='syllabus')
try:
- course.syllabus = get_modulestore(temploc).get_item(temploc).definition['data']
+ course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='overview')
try:
- course.overview = get_modulestore(temploc).get_item(temploc).definition['data']
+ course.overview = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='effort')
try:
- course.effort = get_modulestore(temploc).get_item(temploc).definition['data']
+ course.effort = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
temploc = temploc._replace(name='video')
try:
- raw_video = get_modulestore(temploc).get_item(temploc).definition['data']
+ raw_video = get_modulestore(temploc).get_item(temploc).data
course.intro_video = CourseDetails.parse_video_tag(raw_video)
except ItemNotFoundError:
pass
@@ -116,7 +117,7 @@ class CourseDetails(object):
descriptor.enrollment_end = converted
if dirty:
- get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
+ get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
# to make faster, could compare against db or could have client send over a list of which fields changed.
@@ -133,7 +134,6 @@ class CourseDetails(object):
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag)
-
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
return CourseDetails.fetch(course_location)
diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py
index 3d0b8f78af..b20fb71f66 100644
--- a/cms/djangoapps/models/settings/course_grading.py
+++ b/cms/djangoapps/models/settings/course_grading.py
@@ -2,6 +2,7 @@ from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
import re
from util import converters
+from datetime import timedelta
class CourseGradingModel(object):
@@ -91,7 +92,7 @@ class CourseGradingModel(object):
descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
- get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
+ get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location)
@@ -119,7 +120,7 @@ class CourseGradingModel(object):
else:
descriptor.raw_grader.append(grader)
- get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
+ get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
@@ -134,7 +135,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = cutoffs
- get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
+ get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return cutoffs
@@ -156,11 +157,11 @@ class CourseGradingModel(object):
graceperiodjson = graceperiodjson['grace_period']
# lms requires these to be in a fixed order
- grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson)
+ grace_timedelta = timedelta(**graceperiodjson)
descriptor = get_modulestore(course_location).get_item(course_location)
- descriptor.metadata['graceperiod'] = grace_rep
- get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
+ descriptor.lms.graceperiod = grace_timedelta
+ get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod
def delete_grader(course_location, index):
@@ -176,7 +177,7 @@ class CourseGradingModel(object):
del descriptor.raw_grader[index]
# force propagation to definition
descriptor.raw_grader = descriptor.raw_grader
- get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
+ get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
# NOTE cannot delete cutoffs. May be useful to reset
@staticmethod
@@ -189,7 +190,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
- get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
+ get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
return descriptor.grade_cutoffs
@@ -202,8 +203,8 @@ class CourseGradingModel(object):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
- if 'graceperiod' in descriptor.metadata: del descriptor.metadata['graceperiod']
- get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
+ del descriptor.lms.graceperiod
+ get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata)
@staticmethod
def get_section_grader_type(location):
@@ -212,7 +213,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
return {
- "graderType": descriptor.metadata.get('format', u"Not Graded"),
+ "graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location,
"id": 99 # just an arbitrary value to
}
@@ -224,23 +225,41 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
- descriptor.metadata['format'] = jsondict.get('graderType')
- descriptor.metadata['graded'] = True
+ descriptor.lms.format = jsondict.get('graderType')
+ descriptor.lms.graded = True
else:
- if 'format' in descriptor.metadata: del descriptor.metadata['format']
- if 'graded' in descriptor.metadata: del descriptor.metadata['graded']
+ del descriptor.lms.format
+ del descriptor.lms.graded
- get_modulestore(location).update_metadata(location, descriptor.metadata)
+ get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
@staticmethod
def convert_set_grace_period(descriptor):
- # 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59}
- rawgrace = descriptor.metadata.get('graceperiod', None)
+ # 5 hours 59 minutes 59 seconds => converted to iso format
+ rawgrace = descriptor.lms.graceperiod
if rawgrace:
- parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
- return parsedgrace
- else: return None
+ hours_from_days = rawgrace.days*24
+ seconds = rawgrace.seconds
+ hours_from_seconds = int(seconds / 3600)
+ hours = hours_from_days + hours_from_seconds
+ seconds -= hours_from_seconds * 3600
+ minutes = int(seconds / 60)
+ seconds -= minutes * 60
+
+ graceperiod = {'hours': 0, 'minutes': 0, 'seconds': 0}
+ if hours > 0:
+ graceperiod['hours'] = hours
+
+ if minutes > 0:
+ graceperiod['minutes'] = minutes
+
+ if seconds > 0:
+ graceperiod['seconds'] = seconds
+
+ return graceperiod
+ else:
+ return None
@staticmethod
def parse_grader(json_grader):
diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py
index 24245a39d5..ed11a6d7a4 100644
--- a/cms/djangoapps/models/settings/course_metadata.py
+++ b/cms/djangoapps/models/settings/course_metadata.py
@@ -1,6 +1,8 @@
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor
+from xmodule.modulestore.inheritance import own_metadata
+from xblock.core import Scope
class CourseMetadata(object):
@@ -8,8 +10,7 @@ class CourseMetadata(object):
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
'''
- # __new_advanced_key__ is used by client not server; so, could argue against it being here
- FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
+ FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod']
@classmethod
def fetch(cls, course_location):
@@ -23,17 +24,20 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location)
- for k, v in descriptor.metadata.iteritems():
- if k not in cls.FILTERED_LIST:
- course[k] = v
-
+ for field in descriptor.fields + descriptor.lms.fields:
+ if field.scope != Scope.settings:
+ continue
+
+ if field.name not in cls.FILTERED_LIST:
+ course[field.name] = field.read_from(descriptor)
+
return course
@classmethod
def update_from_json(cls, course_location, jsondict):
"""
Decode the json into CourseMetadata and save any changed attrs to the db.
-
+
Ensures none of the fields are in the blacklist.
"""
descriptor = get_modulestore(course_location).get_item(course_location)
@@ -42,12 +46,18 @@ class CourseMetadata(object):
for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload?
- if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v):
+ if k in cls.FILTERED_LIST:
+ continue
+
+ if hasattr(descriptor, k) and getattr(descriptor, k) != v:
dirty = True
- descriptor.metadata[k] = v
+ setattr(descriptor, k, v)
+ elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k:
+ dirty = True
+ setattr(descriptor.lms, k, v)
if dirty:
- get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
+ get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
@@ -61,10 +71,11 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']:
- if key in descriptor.metadata:
- del descriptor.metadata[key]
+ if hasattr(descriptor, key):
+ delattr(descriptor, key)
+ elif hasattr(descriptor.lms, key):
+ delattr(descriptor.lms, key)
- get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
+ get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
return cls.fetch(course_location)
-
\ No newline at end of file
diff --git a/cms/envs/dev.py b/cms/envs/dev.py
index 9164c02e3f..f70f22512e 100644
--- a/cms/envs/dev.py
+++ b/cms/envs/dev.py
@@ -96,6 +96,13 @@ CACHES = {
'KEY_PREFIX': 'general',
'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key',
+ },
+
+ 'mongo_metadata_inheritance': {
+ 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
+ 'LOCATION': '/var/tmp/mongo_metadata_inheritance',
+ 'TIMEOUT': 300,
+ 'KEY_FUNCTION': 'util.memcache.safe_key',
}
}
diff --git a/cms/envs/test.py b/cms/envs/test.py
index abe03edd41..d7992cb471 100644
--- a/cms/envs/test.py
+++ b/cms/envs/test.py
@@ -98,6 +98,13 @@ CACHES = {
'KEY_PREFIX': 'general',
'VERSION': 4,
'KEY_FUNCTION': 'util.memcache.safe_key',
+ },
+
+ 'mongo_metadata_inheritance': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'LOCATION': '/var/tmp/mongo_metadata_inheritance',
+ 'TIMEOUT': 300,
+ 'KEY_FUNCTION': 'util.memcache.safe_key',
}
}
diff --git a/cms/one_time_startup.py b/cms/one_time_startup.py
index 93428a3404..38a2fef847 100644
--- a/cms/one_time_startup.py
+++ b/cms/one_time_startup.py
@@ -1,5 +1,13 @@
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
+from xmodule.modulestore.django import modulestore
+
+from django.core.cache import get_cache, InvalidCacheBackendError
+
+cache = get_cache('mongo_metadata_inheritance')
+for store_name in settings.MODULESTORE:
+ store = modulestore(store_name)
+ store.metadata_inheritance_cache = cache
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
diff --git a/cms/static/client_templates/advanced_entry.html b/cms/static/client_templates/advanced_entry.html
index 0312fdd344..6be22e2116 100644
--- a/cms/static/client_templates/advanced_entry.html
+++ b/cms/static/client_templates/advanced_entry.html
@@ -1,16 +1,11 @@
-
+
-
- Keys are case sensitive and cannot contain spaces or start with a number
+
\ No newline at end of file
diff --git a/cms/static/js/models/settings/advanced.js b/cms/static/js/models/settings/advanced.js
index 5741eb88dd..adc259239d 100644
--- a/cms/static/js/models/settings/advanced.js
+++ b/cms/static/js/models/settings/advanced.js
@@ -1,27 +1,15 @@
if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS.Models.Settings.Advanced = Backbone.Model.extend({
- // the key for a newly added policy-- before the user has entered a key value
- new_key : "__new_advanced_key__",
defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server)
},
// which keys to send as the deleted keys on next save
deleteKeys : [],
- blacklistKeys : [], // an array which the controller should populate directly for now [static not instance based]
validate: function (attrs) {
- var errors = {};
- for (var key in attrs) {
- if (key === this.new_key || _.isEmpty(key)) {
- errors[key] = "A key must be entered.";
- }
- else if (_.contains(this.blacklistKeys, key)) {
- errors[key] = key + " is a reserved keyword or can be edited on another screen";
- }
- }
- if (!_.isEmpty(errors)) return errors;
+ // Keys can no longer be edited. We are currently not validating values.
},
save : function (attrs, options) {
diff --git a/cms/static/js/template_loader.js b/cms/static/js/template_loader.js
index 3492ca677a..f64c685364 100644
--- a/cms/static/js/template_loader.js
+++ b/cms/static/js/template_loader.js
@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
- templateVersion: "0.0.15",
+ templateVersion: "0.0.16",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {
diff --git a/cms/static/js/views/settings/advanced_view.js b/cms/static/js/views/settings/advanced_view.js
index 9328f843ad..d367b197e7 100644
--- a/cms/static/js/views/settings/advanced_view.js
+++ b/cms/static/js/views/settings/advanced_view.js
@@ -6,14 +6,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
// Model class is CMS.Models.Settings.Advanced
events : {
- 'click .delete-button' : "deleteEntry",
- 'click .new-button' : "addEntry",
- // update model on changes
- 'change .policy-key' : "updateKey",
- // keypress to catch alpha keys and backspace/delete on some browsers
- 'keypress .policy-key' : "showSaveCancelButtons",
- // keyup to catch backspace/delete reliably
- 'keyup .policy-key' : "showSaveCancelButtons",
'focus :input' : "focusInput",
'blur :input' : "blurInput"
// TODO enable/disable save based on validation (currently enabled whenever there are changes)
@@ -95,16 +87,11 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
mirror.setValue(stringValue);
} catch(quotedE) {
// TODO: validation error
- console.log("Error with JSON, even after converting to String.");
- console.log(quotedE);
+ // console.log("Error with JSON, even after converting to String.");
+ // console.log(quotedE);
JSONValue = undefined;
}
}
- else {
- // TODO: validation error
- console.log("Error with JSON, but will not convert to String.");
- console.log(e);
- }
}
if (JSONValue !== undefined) {
self.clearValidationErrors();
@@ -113,7 +100,6 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
}
});
},
-
showMessage: function (type) {
$(".wrapper-alert").removeClass("is-shown");
if (type) {
@@ -128,56 +114,19 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
else {
// This is the case of the page first rendering, or when Cancel is pressed.
this.hideSaveCancelButtons();
- this.toggleNewButton(true);
}
},
-
showSaveCancelButtons: function(event) {
if (!this.buttonsVisible) {
- if (event && (event.type === 'keypress' || event.type === 'keyup')) {
- // check whether it's really an altering event: note, String.fromCharCode(keyCode) will
- // give positive values for control/command/option-letter combos; so, don't use it
- if (!((event.charCode && String.fromCharCode(event.charCode) !== "") ||
- // 8 = backspace, 46 = delete
- event.keyCode === 8 || event.keyCode === 46)) return;
- }
this.$el.find(".message-status").removeClass("is-shown");
$('.wrapper-notification').removeClass('is-hiding').addClass('is-shown');
this.buttonsVisible = true;
}
},
-
hideSaveCancelButtons: function() {
$('.wrapper-notification').removeClass('is-shown').addClass('is-hiding');
this.buttonsVisible = false;
},
-
- toggleNewButton: function (enable) {
- var newButton = this.$el.find(".new-button");
- if (enable) {
- newButton.removeClass('disabled');
- }
- else {
- newButton.addClass('disabled');
- }
- },
-
- deleteEntry : function(event) {
- event.preventDefault();
- // find out which entry
- var li$ = $(event.currentTarget).closest('li');
- // Not data b/c the validation view uses it for a selector
- var key = $('.key', li$).attr('id');
-
- delete this.selectorToField[this.fieldToSelectorMap[key]];
- delete this.fieldToSelectorMap[key];
- if (key !== this.model.new_key) {
- this.model.deleteKeys.push(key);
- this.model.unset(key);
- }
- li$.remove();
- this.showSaveCancelButtons();
- },
saveView : function(event) {
// TODO one last verification scan:
// call validateKey on each to ensure proper format
@@ -201,102 +150,15 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
error : CMS.ServerError
});
},
- addEntry : function() {
- var listEle$ = this.$el.find('.course-advanced-policy-list');
- var newEle = this.renderTemplate("", "");
- listEle$.append(newEle);
- // need to re-find b/c replaceWith seems to copy rather than use the specific ele instance
- var policyValueDivs = this.$el.find('#' + this.model.new_key).closest('li').find('.json');
- // only 1 but hey, let's take advantage of the context mechanism
- _.each(policyValueDivs, this.attachJSONEditor, this);
- this.toggleNewButton(false);
- },
- updateKey : function(event) {
- var parentElement = $(event.currentTarget).closest('.key');
- // old key: either the key as in the model or new_key.
- // That is, it doesn't change as the val changes until val is accepted.
- var oldKey = parentElement.attr('id');
- // TODO: validation of keys with spaces. For now at least trim strings to remove initial and
- // trailing whitespace
- var newKey = $.trim($(event.currentTarget).val());
- if (oldKey !== newKey) {
- // TODO: is it OK to erase other validation messages?
- this.clearValidationErrors();
-
- if (!this.validateKey(oldKey, newKey)) return;
-
- if (this.model.has(newKey)) {
- var error = {};
- error[oldKey] = 'You have already defined "' + newKey + '" in the manual policy definitions.';
- error[newKey] = "You tried to enter a duplicate of this key.";
- this.model.trigger("invalid", this.model, error);
- return false;
- }
-
- // explicitly call validate to determine whether to proceed (relying on triggered error means putting continuation in the success
- // method which is uglier I think?)
- var newEntryModel = {};
- // set the new key's value to the old one's
- newEntryModel[newKey] = (oldKey === this.model.new_key ? '' : this.model.get(oldKey));
-
- var validation = this.model.validate(newEntryModel);
- if (validation) {
- if (_.has(validation, newKey)) {
- // swap to the key which the map knows about
- validation[oldKey] = validation[newKey];
- }
- this.model.trigger("invalid", this.model, validation);
- // abandon update
- return;
- }
-
- // Now safe to actually do the update
- this.model.set(newEntryModel);
-
- // update maps
- var selector = this.fieldToSelectorMap[oldKey];
- this.selectorToField[selector] = newKey;
- this.fieldToSelectorMap[newKey] = selector;
- delete this.fieldToSelectorMap[oldKey];
-
- if (oldKey !== this.model.new_key) {
- // mark the old key for deletion and delete from field maps
- this.model.deleteKeys.push(oldKey);
- this.model.unset(oldKey) ;
- }
- else {
- // id for the new entry will now be the key value. Enable new entry button.
- this.toggleNewButton(true);
- }
-
- // check for newkey being the name of one which was previously deleted in this session
- var wasDeleting = this.model.deleteKeys.indexOf(newKey);
- if (wasDeleting >= 0) {
- this.model.deleteKeys.splice(wasDeleting, 1);
- }
-
- // Update the ID to the new value.
- parentElement.attr('id', newKey);
-
- }
- },
- validateKey : function(oldKey, newKey) {
- // model validation can't handle malformed keys nor notice if 2 fields have same key; so, need to add that chk here
- // TODO ensure there's no spaces or illegal chars (note some checking for spaces currently done in model's
- // validate method.
- return true;
- },
-
renderTemplate: function (key, value) {
var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4),
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
- this.fieldToSelectorMap[(_.isEmpty(key) ? this.model.new_key : key)] = newKeyId;
- this.selectorToField[newKeyId] = (_.isEmpty(key) ? this.model.new_key : key);
+ this.fieldToSelectorMap[key] = newKeyId;
+ this.selectorToField[newKeyId] = key;
return newEle;
},
-
focusInput : function(event) {
$(event.target).prev().addClass("is-focused");
},
diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss
index 5fd05d27a6..25b86336b9 100644
--- a/cms/static/sass/_base.scss
+++ b/cms/static/sass/_base.scss
@@ -472,6 +472,21 @@ textarea.text {
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0;
}
+
+ &[disabled] {
+ border-color: $gray-l4;
+ color: $gray-l2;
+ }
+
+ &[readonly] {
+ border-color: $gray-l4;
+ color: $gray-l1;
+
+ &:focus {
+ @include linear-gradient($lightGrey, tint($lightGrey, 90%));
+ outline: 0;
+ }
+ }
}
// forms - specific
diff --git a/cms/static/sass/_settings.scss b/cms/static/sass/_settings.scss
index d8011dd651..a42ff80bc2 100644
--- a/cms/static/sass/_settings.scss
+++ b/cms/static/sass/_settings.scss
@@ -239,13 +239,9 @@ body.course.settings {
// not editable fields
.field.is-not-editable {
-
- label, .label {
- color: $gray-l3;
- }
-
- input {
- opacity: 0.25;
+
+ & label.is-focused {
+ color: $gray-d2;
}
}
diff --git a/cms/templates/base.html b/cms/templates/base.html
index 387b45a0d1..1ca7a1d79f 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -6,12 +6,12 @@
- <%block name="title">%block> |
- % if context_course:
- <% ctx_loc = context_course.location %>
- ${context_course.display_name} |
- % endif
- edX Studio
+ <%block name="title">%block> |
+ % if context_course:
+ <% ctx_loc = context_course.location %>
+ ${context_course.display_name_with_default} |
+ % endif
+ edX Studio
@@ -66,4 +66,4 @@
<%block name="jsextra">%block>