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 ce2f2e3b87..6690bb7df0 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -12,7 +12,7 @@ profile=no
# Add files or directories to the blacklist. They should be base names, not
# paths.
-ignore=CVS
+ignore=CVS, migrations
# Pickle collected data for later comparisons.
persistent=yes
@@ -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]
@@ -43,7 +51,7 @@ disable=E1102,W0142
output-format=text
# Include message's id in output
-include-ids=no
+include-ids=yes
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
@@ -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_]*)|(__.*__))$
+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/.ruby-version b/.ruby-version
index dd472cffa2..311baaf3e2 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-1.8.7-p371
\ No newline at end of file
+1.9.3-p374
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 c0ab9ec60e..d04e1a6332 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -6,28 +6,27 @@ 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 mock import Mock
-from json import dumps, loads
+from json import loads
-from student.models import Registration
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
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
-from xmodule.modulestore.django import modulestore, _MODULESTORES
+from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
-from xmodule.templates import update_templates
+from xmodule.modulestore.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
@@ -63,7 +62,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=uname, password=password)
-
def check_edit_unit(self, test_course_name):
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
@@ -82,8 +80,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
- ms = modulestore('direct')
- course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+ module_store = modulestore('direct')
+ course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# reverse the ordering
reverse_tabs = []
@@ -91,9 +89,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
if tab['type'] == 'static_tab':
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
- resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
+ self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
- course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+ course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# compare to make sure that the tabs information is in the expected order after the server call
course_tabs = []
@@ -103,29 +101,61 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs)
+ def test_delete(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+
+ module_store = modulestore('direct')
+
+ sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
+
+ 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.children)
+
+ self.client.post(reverse('delete_item'),
+ json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
+ "application/json")
+
+ found = False
+ try:
+ module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
+ found = True
+ except ItemNotFoundError:
+ pass
+
+ self.assertFalse(found)
+
+ 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.children)
+
+
+
def test_about_overrides(self):
'''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
while there is a base definition in /about/effort.html
'''
import_from_xml(modulestore(), 'common/test/data/', ['full'])
- ms = modulestore('direct')
- effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
- self.assertEqual(effort.definition['data'], '6 hours')
+ module_store = modulestore('direct')
+ effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
+ self.assertEqual(effort.data, '6 hours')
# this one should be in a non-override folder
- effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
- self.assertEqual(effort.definition['data'], 'TBD')
+ effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
+ self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
- ms = modulestore('direct')
- cs = contentstore()
+ module_store = modulestore('direct')
+ content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
- course = ms.get_item(source_location)
- self.assertNotIn('hide_progress_tab', course.metadata)
+ course = module_store.get_item(source_location)
+ self.assertFalse(course.hide_progress_tab)
def test_clone_course(self):
@@ -143,19 +173,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
- ms = modulestore('direct')
- cs = contentstore()
+ module_store = modulestore('direct')
+ content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
- clone_course(ms, cs, source_location, dest_location)
+ clone_course(module_store, content_store, source_location, dest_location)
# now loop through all the units in the course and verify that the clone can render them, which
# means the objects are at least present
- items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
+ items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
- clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
+ clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
self.assertGreater(len(clone_items), 0)
for descriptor in items:
new_loc = descriptor.location._replace(org='MITx', course='999')
@@ -166,14 +196,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
- ms = modulestore('direct')
- cs = contentstore()
+ module_store = modulestore('direct')
+ content_store = contentstore()
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
- delete_course(ms, cs, location, commit=True)
+ delete_course(module_store, content_store, location, commit=True)
- items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
+ items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0)
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
@@ -188,10 +218,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(fs.exists(item.location.name + filename_suffix))
def test_export_course(self):
- ms = modulestore('direct')
- cs = contentstore()
+ module_store = modulestore('direct')
+ content_store = contentstore()
- import_from_xml(ms, 'common/test/data/', ['full'])
+ import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
root_dir = path(mkdtemp_clean())
@@ -199,43 +229,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
- export_to_xml(ms, cs, location, root_dir, 'test_export')
+ export_to_xml(module_store, content_store, location, root_dir, 'test_export')
# check for static tabs
- self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
+ self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
# check for custom_tags
- self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
+ self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html')
# check for custom_tags
- self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
+ self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for graiding_policy.json
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(fs.exists('grading_policy.json'))
- course = ms.get_item(location)
+ course = module_store.get_item(location)
# compare what's on disk compared to what we have in our course
- with fs.open('grading_policy.json','r') as grading_policy:
- on_disk = loads(grading_policy.read())
- self.assertEqual(on_disk, course.definition['data']['grading_policy'])
+ with fs.open('grading_policy.json', 'r') as grading_policy:
+ on_disk = loads(grading_policy.read())
+ self.assertEqual(on_disk, course.grading_policy)
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
# compare what's on disk to what we have in the course module
- with fs.open('policy.json','r') as course_policy:
+ 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(ms, cs, location)
+ delete_course(module_store, content_store, location)
# reimport
- import_from_xml(ms, root_dir, ['test_export'])
+ import_from_xml(module_store, root_dir, ['test_export'])
- items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
+ items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
for descriptor in items:
print "Checking {0}....".format(descriptor.location.url())
@@ -245,11 +275,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self):
- ms = modulestore('direct')
- cs = contentstore()
+ module_store = modulestore('direct')
+ content_store = contentstore()
# import a test course
- import_from_xml(ms, 'common/test/data/', ['full'])
+ import_from_xml(module_store, 'common/test/data/', ['full'])
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
@@ -264,32 +294,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
def test_export_course_with_unknown_metadata(self):
- ms = modulestore('direct')
- cs = contentstore()
+ module_store = modulestore('direct')
+ content_store = contentstore()
- import_from_xml(ms, 'common/test/data/', ['full'])
+ import_from_xml(module_store, '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)
+ 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
- ms.update_metadata(location, course.metadata)
+ module_store.update_metadata(location, metadata)
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
- bExported = False
+ exported = False
try:
- export_to_xml(ms, cs, location, root_dir, 'test_export')
- bExported = True
+ export_to_xml(module_store, content_store, location, root_dir, 'test_export')
+ exported = True
except Exception:
pass
- self.assertTrue(bExported)
+ self.assertTrue(exported)
class ContentStoreTest(ModuleStoreTestCase):
"""
@@ -428,7 +459,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_capa_module(self):
"""Test that a problem treats markdown specially."""
- course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
+ CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
@@ -445,82 +476,77 @@ 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'])
- ms = modulestore('direct')
+ module_store = modulestore('direct')
did_load_item = False
- try:
- ms.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
+ 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'])
- ms = modulestore('direct')
- course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+ module_store = modulestore('direct')
+ course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
- verticals = ms.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
+ verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
- self.assertIn('xqa_key', vertical.metadata)
- self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
+ self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
self.assertGreater(len(verticals), 0)
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
-
+
# crate a new module and add it as a child to a vertical
- ms.clone_item(source_template_location, new_component_location)
+ module_store.clone_item(source_template_location, new_component_location)
parent = verticals[0]
- ms.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
- ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
- new_module = ms.get_item(new_component_location)
+ 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'
- ms.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
- ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
- new_module = ms.get_item(new_component_location)
+ 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):
- ms = modulestore('direct')
+ def test_template_cleanup(self):
+ module_store = modulestore('direct')
# insert a bogus template in the store
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
-
- ms.clone_item(source_template_location, bogus_template_location)
- verify_create = ms.get_item(bogus_template_location)
+ module_store.clone_item(source_template_location, bogus_template_location)
+
+ verify_create = module_store.get_item(bogus_template_location)
self.assertIsNotNone(verify_create)
# now run cleanup
@@ -529,10 +555,8 @@ class TemplateTestCase(ModuleStoreTestCase):
# now try to find dangling template, it should not be in DB any longer
asserted = False
try:
- verify_create = ms.get_item(bogus_template_location)
+ verify_create = module_store.get_item(bogus_template_location)
except ItemNotFoundError:
asserted = True
- self.assertTrue(asserted)
-
-
+ 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 345861f979..80833a4e5f 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'
@@ -86,12 +90,14 @@ def signup(request):
csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token})
+
def old_login_redirect(request):
'''
Redirect to the active login url.
'''
return redirect('login', permanent=True)
+
@ssl_login_shortcut
@ensure_csrf_cookie
def login_page(request):
@@ -104,14 +110,16 @@ def login_page(request):
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
})
+
def howitworks(request):
if request.user.is_authenticated():
return index(request)
- else:
+ else:
return render_to_response('howitworks.html', {})
# ==== Views for any logged-in user ==================================
+
@login_required
@ensure_csrf_cookie
def index(request):
@@ -131,7 +139,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,
@@ -145,6 +153,7 @@ def index(request):
# ==== Views with per-item permissions================================
+
def has_access(user, location, role=STAFF_ROLE_NAME):
'''
Return True if user allowed to access this piece of data
@@ -234,8 +243,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()
@@ -288,8 +302,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)
@@ -310,10 +323,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 = [
@@ -357,11 +370,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',
@@ -372,11 +380,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,
})
@@ -393,6 +401,7 @@ def preview_component(request, location):
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
+
@expect_json
@login_required
@ensure_csrf_cookie
@@ -440,9 +449,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)
@@ -453,46 +461,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
@@ -500,6 +471,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
@@ -510,6 +508,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?
@@ -520,6 +526,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,
)
@@ -532,11 +539,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
@@ -548,12 +555,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':
@@ -571,11 +579,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
@@ -589,7 +595,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
@@ -636,6 +642,19 @@ def delete_item(request):
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
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)
+
+ 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()
@@ -673,7 +692,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
@@ -681,15 +700,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()
@@ -709,6 +728,7 @@ def create_draft(request):
return HttpResponse()
+
@login_required
@expect_json
def publish_draft(request):
@@ -738,6 +758,7 @@ def unpublish_unit(request):
return HttpResponse()
+
@login_required
@expect_json
def clone_item(request):
@@ -754,22 +775,18 @@ 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()}))
-#@login_required
-#@ensure_csrf_cookie
+
def upload_asset(request, org, course, coursename):
'''
cdodge: this method allows for POST uploading of files into the course asset library, which will
@@ -831,6 +848,7 @@ def upload_asset(request, org, course, coursename):
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
+
'''
This view will return all CMS users who are editors for the specified course
'''
@@ -863,6 +881,7 @@ def create_json_response(errmsg = None):
return resp
+
'''
This POST-back view will add a user - specified by email - to the list of editors for
the specified course
@@ -895,6 +914,7 @@ def add_user(request, location):
return create_json_response()
+
'''
This POST-back view will remove a user - specified by email - from the list of editors for
the specified course
@@ -926,6 +946,7 @@ def remove_user(request, location):
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
+
@login_required
@ensure_csrf_cookie
def static_pages(request, org, course, coursename):
@@ -980,7 +1001,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:
@@ -989,7 +1010,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()
@@ -1029,6 +1050,7 @@ def edit_tabs(request, org, course, coursename):
'components': components
})
+
def not_found(request):
return render_to_response('error.html', {'error': '404'})
@@ -1064,6 +1086,7 @@ def course_info(request, org, course, name, provided_id=None):
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
+
@expect_json
@login_required
@ensure_csrf_cookie
@@ -1161,6 +1184,7 @@ def get_course_settings(request, org, course, name):
"section": "details"})
})
+
@login_required
@ensure_csrf_cookie
def course_config_graders_page(request, org, course, name):
@@ -1184,6 +1208,7 @@ def course_config_graders_page(request, org, course, name):
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
})
+
@login_required
@ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name):
@@ -1203,10 +1228,10 @@ 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)),
})
+
@expect_json
@login_required
@ensure_csrf_cookie
@@ -1238,6 +1263,7 @@ def course_settings_updates(request, org, course, name, section):
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json")
+
@expect_json
@login_required
@ensure_csrf_cookie
@@ -1272,7 +1298,7 @@ def course_grader_updates(request, org, course, name, grader_index=None):
return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course', name]), request.POST)),
mimetype="application/json")
-
+
## NB: expect_json failed on ["key", "key2"] and json payload
@login_required
@ensure_csrf_cookie
@@ -1284,7 +1310,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()
@@ -1294,7 +1320,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':
@@ -1363,6 +1389,7 @@ def asset_index(request, org, course, name):
def edge(request):
return render_to_response('university_profiles/edge.html', {})
+
@login_required
@expect_json
def create_new_course(request):
@@ -1404,13 +1431,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)
@@ -1418,6 +1442,7 @@ def create_new_course(request):
return HttpResponse(json.dumps({'id': new_course.location.url()}))
+
def initialize_course_tabs(course):
# set up the default tabs
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
@@ -1428,12 +1453,13 @@ 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
@login_required
@@ -1512,6 +1538,7 @@ def import_course(request, org, course, name):
course_module.location.name])
})
+
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
@@ -1563,6 +1590,7 @@ def export_course(request, org, course, name):
'successful_import_redirect_url': ''
})
+
def event(request):
'''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
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 d088d75665..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,9 +10,8 @@ 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):
"""
@@ -18,53 +19,63 @@ class CourseMetadata(object):
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
-
+
course = {}
-
+
descriptor = get_modulestore(course_location).get_item(course_location)
-
- for k, v in descriptor.metadata.iteritems():
- if k not in cls.FILTERED_LIST:
- course[k] = v
-
+
+ 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)
-
+
dirty = False
for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload?
- if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v):
+ 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
return cls.fetch(course_location)
-
+
@classmethod
def delete_key(cls, course_location, payload):
'''
Remove the given metadata key(s) from the course. payload can be a single key or [key..]
'''
descriptor = get_modulestore(course_location).get_item(course_location)
-
+
for key in payload['deleteKeys']:
- if key in descriptor.metadata:
- del descriptor.metadata[key]
-
- get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
-
+ if hasattr(descriptor, key):
+ delattr(descriptor, key)
+ elif hasattr(descriptor.lms, key):
+ delattr(descriptor.lms, key)
+
+ 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 a933bbdb9b..e3ff098efb 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) {
this.$el.find(".message-status").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').addClass('is-shown');
this.buttonsVisible = true;
}
},
-
hideSaveCancelButtons: function() {
$('.wrapper-notification').removeClass('is-shown');
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 5d4bc7c773..a2d46c0510 100644
--- a/cms/static/sass/_base.scss
+++ b/cms/static/sass/_base.scss
@@ -405,6 +405,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 498897bd11..f7b2c46f61 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -6,10 +6,10 @@
- <%block name="title">%block> |
+ <%block name="title">%block> |
% if context_course:
<% ctx_loc = context_course.location %>
- ${context_course.display_name} |
+ ${context_course.display_name_with_default} |
% endif
edX Studio
@@ -22,7 +22,7 @@
-
+
<%block name="header_extras">%block>
diff --git a/cms/templates/course_index.html b/cms/templates/course_index.html
deleted file mode 100644
index 5c8772c1ed..0000000000
--- a/cms/templates/course_index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<%inherit file="base.html" />
-
-<%include file="widgets/header.html"/>
-
-<%block name="content">
-
-
- <%include file="widgets/navigation.html"/>
-
-
-
-
-
-%block>
diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html
index 43775122d4..42ddc39be1 100644
--- a/cms/templates/edit_subsection.html
+++ b/cms/templates/edit_subsection.html
@@ -22,7 +22,7 @@
<%
- start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
- parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.start is not None else None
+ start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
+ parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
%>
-
+
- % if subsection.start != parent_item.start and subsection.start:
+ % if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
% if parent_start_date is None:
-
The date above differs from the release date of ${parent_item.display_name}, which is unset.
+
The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
% else:
-
The date above differs from the release date of ${parent_item.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
+
The date above differs from the release date of ${parent_item.display_name_with_default} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
% endif
- Sync to ${parent_item.display_name}.
<%
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
- due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
+ due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None
%>
-
+
Remove due date
@@ -110,7 +110,7 @@
-
+
%block>
\ No newline at end of file
diff --git a/cms/templates/new_item.html b/cms/templates/new_item.html
index 60da39fd2a..45cb157845 100644
--- a/cms/templates/new_item.html
+++ b/cms/templates/new_item.html
@@ -8,7 +8,7 @@
<%
- start_date = datetime.fromtimestamp(mktime(section.start)) if section.start is not None else None
+ start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
%>
@@ -174,9 +174,9 @@
Will Release: ${start_date_str} at ${start_time_str}Edit
%endif
-
diff --git a/cms/templates/settings.html b/cms/templates/settings.html
index 39e9b70f9d..b26a17125b 100644
--- a/cms/templates/settings.html
+++ b/cms/templates/settings.html
@@ -70,17 +70,17 @@ from contentstore import utils
-
+
-
+
-
+
These are used in your course URL, and cannot be changed
diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html
index ceee406398..838af5ada9 100644
--- a/cms/templates/settings_advanced.html
+++ b/cms/templates/settings_advanced.html
@@ -21,7 +21,6 @@ $(document).ready(function () {
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
var advancedModel = new CMS.Models.Settings.Advanced(${advanced_dict | n}, {parse: true});
- advancedModel.blacklistKeys = ${advanced_blacklist | n};
advancedModel.url = "${reverse('course_advanced_settings_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
var editor = new CMS.Views.Settings.Advanced({
@@ -61,18 +60,11 @@ editor.render();
Manually Edit Course Policy Values (JSON Key / Value pairs)
-
Warning: Add only manual policy data that you are familiar
- with.
+
Warning: Do not modify these policies unless you are familiar with their purpose.
- %endfor
-
-
-
- + Add New Section
-
-
-
-
-
-
-
diff --git a/cms/templates/widgets/sequence-edit.html b/cms/templates/widgets/sequence-edit.html
index e9d796784d..c70f2568fa 100644
--- a/cms/templates/widgets/sequence-edit.html
+++ b/cms/templates/widgets/sequence-edit.html
@@ -40,7 +40,7 @@
${child.display_name}
+ data-preview-type="${child.module_class.js_module_name}">${child.display_name_with_default}
handle
%endfor
diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html
index 8e23b05bf8..5ac05e79eb 100644
--- a/cms/templates/widgets/units.html
+++ b/cms/templates/widgets/units.html
@@ -22,7 +22,7 @@ This def will enumerate through a passed in subsection and list all of the units
@@ -39,7 +39,7 @@ This def will enumerate through a passed in subsection and list all of the units
-%def>
+%def>
diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py
new file mode 100644
index 0000000000..391cac8eca
--- /dev/null
+++ b/cms/xmodule_namespace.py
@@ -0,0 +1,30 @@
+import datetime
+
+from xblock.core import Namespace, Boolean, Scope, ModelType, String
+
+
+class StringyBoolean(Boolean):
+ def from_json(self, value):
+ if isinstance(value, basestring):
+ return value.lower() == 'true'
+ return value
+
+class DateTuple(ModelType):
+ """
+ ModelType that stores datetime objects as time tuples
+ """
+ def from_json(self, value):
+ return datetime.datetime(*value[0:6])
+
+ def to_json(self, value):
+ if value is None:
+ return None
+
+ return list(value.timetuple())
+
+
+class CmsNamespace(Namespace):
+ is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
+ published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
+ published_by = String(help="Id of the user who published this module", scope=Scope.settings)
+ empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
diff --git a/common/djangoapps/__init__.py b/common/djangoapps/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index f0234ec71a..c362ed4e89 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -65,23 +65,23 @@ def is_commentable_cohorted(course_id, commentable_id):
ans))
return ans
-
+
def get_cohorted_commentables(course_id):
"""
Given a course_id return a list of strings representing cohorted commentables
"""
course = courses.get_course_by_id(course_id)
-
+
if not course.is_cohorted:
# this is the easy case :)
ans = []
- else:
+ else:
ans = course.cohorted_discussions
return ans
-
-
+
+
def get_cohort(user, course_id):
"""
Given a django User and a course_id, return the user's cohort in that
@@ -120,7 +120,8 @@ def get_cohort(user, course_id):
return None
choices = course.auto_cohort_groups
- if len(choices) == 0:
+ n = len(choices)
+ if n == 0:
# Nowhere to put user
log.warning("Course %s is auto-cohorted, but there are no"
" auto_cohort_groups specified",
@@ -128,12 +129,19 @@ def get_cohort(user, course_id):
return None
# Put user in a random group, creating it if needed
- group_name = random.choice(choices)
+ choice = random.randrange(0, n)
+ group_name = choices[choice]
+
+ # Victor: we are seeing very strange behavior on prod, where almost all users
+ # end up in the same group. Log at INFO to try to figure out what's going on.
+ log.info("DEBUG: adding user {0} to cohort {1}. choice={2}".format(
+ user, group_name,choice))
+
group, created = CourseUserGroup.objects.get_or_create(
course_id=course_id,
group_type=CourseUserGroup.COHORT,
name=group_name)
-
+
user.course_groups.add(group)
return group
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
index efed39d536..94d52ff6df 100644
--- a/common/djangoapps/course_groups/tests/tests.py
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -6,7 +6,7 @@ from django.test.utils import override_settings
from course_groups.models import CourseUserGroup
from course_groups.cohorts import (get_cohort, get_course_cohorts,
- is_commentable_cohorted)
+ is_commentable_cohorted, get_cohort_by_name)
from xmodule.modulestore.django import modulestore, _MODULESTORES
@@ -76,7 +76,7 @@ class TestCohorts(django.test.TestCase):
"id": to_id(name)})
for name in discussions)
- course.metadata["discussion_topics"] = topics
+ course.discussion_topics = topics
d = {"cohorted": cohorted}
if cohorted_discussions is not None:
@@ -88,7 +88,7 @@ class TestCohorts(django.test.TestCase):
if auto_cohort_groups is not None:
d["auto_cohort_groups"] = auto_cohort_groups
- course.metadata["cohort_config"] = d
+ course.cohort_config = d
def setUp(self):
@@ -168,7 +168,7 @@ class TestCohorts(django.test.TestCase):
self.assertEquals(get_cohort(user3, course.id), None,
"No groups->no auto-cohorting")
-
+
# Now make it different
self.config_course_cohorts(course, [], cohorted=True,
auto_cohort=True,
@@ -180,6 +180,37 @@ class TestCohorts(django.test.TestCase):
"user2 should still be in originally placed cohort")
+ def test_auto_cohorting_randomization(self):
+ """
+ Make sure get_cohort() randomizes properly.
+ """
+ course = modulestore().get_course("edX/toy/2012_Fall")
+ self.assertEqual(course.id, "edX/toy/2012_Fall")
+ self.assertFalse(course.is_cohorted)
+
+ groups = ["group_{0}".format(n) for n in range(5)]
+ self.config_course_cohorts(course, [], cohorted=True,
+ auto_cohort=True,
+ auto_cohort_groups=groups)
+
+ # Assign 100 users to cohorts
+ for i in range(100):
+ user = User.objects.create(username="test_{0}".format(i),
+ email="a@b{0}.com".format(i))
+ get_cohort(user, course.id)
+
+ # Now make sure that the assignment was at least vaguely random:
+ # each cohort should have at least 1, and fewer than 50 students.
+ # (with 5 groups, probability of 0 users in any group is about
+ # .8**100= 2.0e-10)
+ for cohort_name in groups:
+ cohort = get_cohort_by_name(course.id, cohort_name)
+ num_users = cohort.users.count()
+ self.assertGreater(num_users, 1)
+ self.assertLess(num_users, 50)
+
+
+
def test_get_course_cohorts(self):
course1_id = 'a/b/c'
course2_id = 'e/f/g'
diff --git a/common/djangoapps/status/tests.py b/common/djangoapps/status/tests.py
index 1695663ac5..bf60017036 100644
--- a/common/djangoapps/status/tests.py
+++ b/common/djangoapps/status/tests.py
@@ -4,7 +4,7 @@ import os
from django.test.utils import override_settings
from tempfile import NamedTemporaryFile
-from status import get_site_status_msg
+from .status import get_site_status_msg
# Get a name where we can put test files
TMP_FILE = NamedTemporaryFile(delete=False)
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 6ac57c1182..902ec82677 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -44,9 +44,8 @@ from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access
-from courseware.models import StudentModuleCache
from courseware.views import get_module_for_descriptor, jump_to
-from courseware.module_render import get_instance_module
+from courseware.model_data import ModelDataCache
from statsd import statsd
@@ -318,7 +317,7 @@ def change_enrollment(request):
if not has_access(user, course, 'enroll'):
return {'success': False,
'error': 'enrollment in {} not allowed at this time'
- .format(course.display_name)}
+ .format(course.display_name_with_default)}
org, course_num, run = course_id.split("/")
statsd.increment("common.student.enrollment",
@@ -1071,14 +1070,14 @@ def accept_name_change(request):
@csrf_exempt
def test_center_login(request):
- # errors are returned by navigating to the error_url, adding a query parameter named "code"
+ # errors are returned by navigating to the error_url, adding a query parameter named "code"
# which contains the error code describing the exceptional condition.
def makeErrorURL(error_url, error_code):
log.error("generating error URL with error code {}".format(error_code))
return "{}?code={}".format(error_url, error_code);
-
+
# get provided error URL, which will be used as a known prefix for returning error messages to the
- # Pearson shell.
+ # Pearson shell.
error_url = request.POST.get("errorURL")
# TODO: check that the parameters have not been tampered with, by comparing the code provided by Pearson
@@ -1089,12 +1088,12 @@ def test_center_login(request):
# calculate SHA for query string
# TODO: figure out how to get the original query string, so we can hash it and compare.
-
-
+
+
if 'clientCandidateID' not in request.POST:
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientCandidateID"));
client_candidate_id = request.POST.get("clientCandidateID")
-
+
# TODO: check remaining parameters, and maybe at least log if they're not matching
# expected values....
# registration_id = request.POST.get("registrationID")
@@ -1108,12 +1107,12 @@ def test_center_login(request):
return HttpResponseRedirect(makeErrorURL(error_url, "invalidClientCandidateID"));
# find testcenter_registration that matches the provided exam code:
- # Note that we could rely in future on either the registrationId or the exam code,
- # or possibly both. But for now we know what to do with an ExamSeriesCode,
+ # Note that we could rely in future on either the registrationId or the exam code,
+ # or possibly both. But for now we know what to do with an ExamSeriesCode,
# while we currently have no record of RegistrationID values at all.
if 'vueExamSeriesCode' not in request.POST:
- # we are not allowed to make up a new error code, according to Pearson,
- # so instead of "missingExamSeriesCode", we use a valid one that is
+ # we are not allowed to make up a new error code, according to Pearson,
+ # so instead of "missingExamSeriesCode", we use a valid one that is
# inaccurate but at least distinct. (Sigh.)
log.error("missing exam series code for cand ID {}".format(client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "missingPartnerID"));
@@ -1127,11 +1126,11 @@ def test_center_login(request):
if not registrations:
log.error("not able to find exam registration for exam {} and cand ID {}".format(exam_series_code, client_candidate_id))
return HttpResponseRedirect(makeErrorURL(error_url, "noTestsAssigned"));
-
+
# TODO: figure out what to do if there are more than one registrations....
# for now, just take the first...
registration = registrations[0]
-
+
course_id = registration.course_id
course = course_from_id(course_id) # assume it will be found....
if not course:
@@ -1149,19 +1148,19 @@ def test_center_login(request):
if not timelimit_descriptor:
log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
-
- timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
- timelimit_descriptor, depth=None)
- timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
+
+ timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
+ timelimit_descriptor, depth=None)
+ timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache, course_id, position=None)
if not timelimit_module.category == 'timelimit':
log.error("cand {} on exam {} for course {}: non-timelimit module at location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"));
-
+
if timelimit_module and timelimit_module.has_ended:
log.warning("cand {} on exam {} for course {}: test already over at {}".format(client_candidate_id, exam_series_code, course_id, timelimit_module.ending_at))
return HttpResponseRedirect(makeErrorURL(error_url, "allTestsTaken"));
-
+
# check if we need to provide an accommodation:
time_accommodation_mapping = {'ET12ET' : 'ADDHALFTIME',
'ET30MN' : 'ADD30MIN',
@@ -1174,27 +1173,24 @@ def test_center_login(request):
# special, hard-coded client ID used by Pearson shell for testing:
if client_candidate_id == "edX003671291147":
time_accommodation_code = 'TESTING'
-
+
if time_accommodation_code:
timelimit_module.accommodation_code = time_accommodation_code
- instance_module = get_instance_module(course_id, testcenteruser.user, timelimit_module, timelimit_module_cache)
- instance_module.state = timelimit_module.get_instance_state()
- instance_module.save()
log.info("cand {} on exam {} for course {}: receiving accommodation {}".format(client_candidate_id, exam_series_code, course_id, time_accommodation_code))
-
+
# UGLY HACK!!!
- # Login assumes that authentication has occurred, and that there is a
+ # Login assumes that authentication has occurred, and that there is a
# backend annotation on the user object, indicating which backend
# against which the user was authenticated. We're authenticating here
# against the registration entry, and assuming that the request given
# this information is correct, we allow the user to be logged in
# without a password. This could all be formalized in a backend object
- # that does the above checking.
+ # that does the above checking.
# TODO: (brian) create a backend class to do this.
- # testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
- testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
+ # testcenteruser.user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
+ testcenteruser.user.backend = "%s.%s" % ("TestcenterAuthenticationModule", "TestcenterAuthenticationClass")
login(request, testcenteruser.user)
-
+
# And start the test:
return jump_to(request, course_id, location)
diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py
index bb7ae012c8..a531f4fd26 100644
--- a/common/djangoapps/terrain/factories.py
+++ b/common/djangoapps/terrain/factories.py
@@ -7,13 +7,14 @@ from xmodule.modulestore.django import modulestore
from time import gmtime
from uuid import uuid4
from xmodule.timeparse import stringify_time
+from xmodule.modulestore.inheritance import own_metadata
class GroupFactory(Factory):
FACTORY_FOR = Group
name = 'staff_MITx/999/Robot_Super_Course'
-
+
class UserProfileFactory(Factory):
FACTORY_FOR = UserProfile
@@ -81,18 +82,17 @@ class XModuleCourseFactory(Factory):
# This metadata code was copied from cms/djangoapps/contentstore/views.py
if display_name is not None:
- new_course.metadata['display_name'] = display_name
+ new_course.display_name = display_name
- new_course.metadata['data_dir'] = uuid4().hex
- new_course.metadata['start'] = stringify_time(gmtime())
+ new_course.lms.start = gmtime()
new_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"}]
# Update the data in the mongo datastore
- store.update_metadata(new_course.location.url(), new_course.own_metadata)
+ store.update_metadata(new_course.location.url(), own_metadata(new_course))
return new_course
@@ -139,17 +139,14 @@ class XModuleItemFactory(Factory):
new_item = store.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
- store.update_metadata(new_item.location.url(), new_item.own_metadata)
+ store.update_metadata(new_item.location.url(), own_metadata(new_item))
if new_item.location.category not in DETACHED_CATEGORIES:
- store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
+ store.update_children(parent_location, parent.children + [new_item.location.url()])
return new_item
diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py
index 88fba697b2..3dcef9b1ed 100644
--- a/common/djangoapps/terrain/steps.py
+++ b/common/djangoapps/terrain/steps.py
@@ -1,5 +1,5 @@
from lettuce import world, step
-from factories import *
+from .factories import *
from lettuce.django import django_url
from django.contrib.auth.models import User
from student.models import CourseEnrollment
diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py
index 7b19c27553..d398dfef0d 100644
--- a/common/djangoapps/xmodule_modifiers.py
+++ b/common/djangoapps/xmodule_modifiers.py
@@ -33,7 +33,7 @@ def wrap_xmodule(get_html, module, template, context=None):
def _get_html():
context.update({
'content': get_html(),
- 'display_name': module.metadata.get('display_name') if module.metadata is not None else None,
+ 'display_name': module.display_name,
'class_': module.__class__.__name__,
'module_name': module.js_module_name
})
@@ -108,42 +108,25 @@ def add_histogram(get_html, module, user):
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
- # TODO (ichuang): Remove after fall 2012 LMS migration done
- if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
- [filepath, filename] = module.definition.get('filename', ['', None])
- osfs = module.system.filestore
- if filename is not None and osfs.exists(filename):
- # if original, unmangled filename exists then use it (github
- # doesn't like symlinks)
- filepath = filename
- data_dir = osfs.root_path.rsplit('/')[-1]
- giturl = module.metadata.get('giturl', 'https://github.com/MITx')
- edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
- else:
- edit_link = False
- # Need to define all the variables that are about to be used
- giturl = ""
- data_dir = ""
- source_file = module.metadata.get('source_file', '') # source used to generate the problem XML, eg latex or word
+ source_file = module.lms.source_file # source used to generate the problem XML, eg latex or word
# useful to indicate to staff if problem has been released or not
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = time.gmtime()
is_released = "unknown"
- mstart = getattr(module.descriptor, 'start')
+ mstart = module.descriptor.lms.start
+
if mstart is not None:
is_released = "Yes!" if (now > mstart) else "Not yet"
- staff_context = {'definition': module.definition.get('data'),
- 'metadata': json.dumps(module.metadata, indent=4),
+ staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields],
+ 'lms_fields': [(field.name, getattr(module.lms, field.name)) for field in module.lms.fields],
'location': module.location,
- 'xqa_key': module.metadata.get('xqa_key', ''),
+ 'xqa_key': module.lms.xqa_key,
'source_file': source_file,
- 'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
'category': str(module.__class__.__name__),
# Template uses element_id in js function names, so can't allow dashes
'element_id': module.location.html_id().replace('-', '_'),
- 'edit_link': edit_link,
'user': user,
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
'histogram': json.dumps(histogram),
diff --git a/common/lib/capa/capa/calc.py b/common/lib/capa/capa/calc.py
index 0f062d17d5..c3fe6b656b 100644
--- a/common/lib/capa/capa/calc.py
+++ b/common/lib/capa/capa/calc.py
@@ -183,7 +183,7 @@ def evaluator(variables, functions, string, cs=False):
# 0.33k or -17
number = (Optional(minus | plus) + inner_number
- + Optional(CaselessLiteral("E") + Optional("-") + number_part)
+ + Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part)
+ Optional(number_suffix))
number = number.setParseAction(number_parse_action) # Convert to number
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index 8b32686985..42753fc90b 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -39,11 +39,11 @@ import verifiers
import verifiers.draganddrop
import calc
-from correctmap import CorrectMap
+from .correctmap import CorrectMap
import eia
import inputtypes
import customrender
-from util import contextualize_text, convert_files_to_filenames
+from .util import contextualize_text, convert_files_to_filenames
import xqueue_interface
# to be replaced with auto-registering
@@ -78,7 +78,7 @@ global_context = {'random': random,
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
# main class for this module
@@ -108,6 +108,8 @@ class LoncapaProblem(object):
self.do_reset()
self.problem_id = id
self.system = system
+ if self.system is None:
+ raise Exception()
self.seed = seed
if state:
diff --git a/common/lib/capa/capa/checker.py b/common/lib/capa/capa/checker.py
index f583a5ea7d..15358aac9e 100755
--- a/common/lib/capa/capa/checker.py
+++ b/common/lib/capa/capa/checker.py
@@ -12,8 +12,8 @@ from path import path
from cStringIO import StringIO
from collections import defaultdict
-from calc import UndefinedVariable
-from capa_problem import LoncapaProblem
+from .calc import UndefinedVariable
+from .capa_problem import LoncapaProblem
from mako.lookup import TemplateLookup
logging.basicConfig(format="%(levelname)s %(message)s")
diff --git a/common/lib/capa/capa/chem/tests.py b/common/lib/capa/capa/chem/tests.py
index 571526f915..f422fcf0d1 100644
--- a/common/lib/capa/capa/chem/tests.py
+++ b/common/lib/capa/capa/chem/tests.py
@@ -2,7 +2,7 @@ import codecs
from fractions import Fraction
import unittest
-from chemcalc import (compare_chemical_expression, divide_chemical_expression,
+from .chemcalc import (compare_chemical_expression, divide_chemical_expression,
render_to_html, chemical_equations_equal)
import miller
@@ -277,7 +277,6 @@ class Test_Render_Equations(unittest.TestCase):
def test_render9(self):
s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-"
- #import ipdb; ipdb.set_trace()
out = render_to_html(s)
correct = u'5[Ni(NH3)4]2++5⁄2SO42-'
log(out + ' ------- ' + correct, 'html')
diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py
index ea56863a2f..b726f765d8 100644
--- a/common/lib/capa/capa/correctmap.py
+++ b/common/lib/capa/capa/correctmap.py
@@ -47,7 +47,7 @@ class CorrectMap(object):
queuestate=None, **kwargs):
if answer_id is not None:
- self.cmap[answer_id] = {'correctness': correctness,
+ self.cmap[str(answer_id)] = {'correctness': correctness,
'npoints': npoints,
'msg': msg,
'hint': hint,
diff --git a/common/lib/capa/capa/customrender.py b/common/lib/capa/capa/customrender.py
index a925a5970d..60d3ce578b 100644
--- a/common/lib/capa/capa/customrender.py
+++ b/common/lib/capa/capa/customrender.py
@@ -6,7 +6,7 @@ These tags do not have state, so they just get passed the system (for access to
and the xml element.
"""
-from registry import TagRegistry
+from .registry import TagRegistry
import logging
import re
@@ -15,9 +15,9 @@ import json
from lxml import etree
import xml.sax.saxutils as saxutils
-from registry import TagRegistry
+from .registry import TagRegistry
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
registry = TagRegistry()
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index f614743e67..c2babfa479 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -47,10 +47,10 @@ import sys
import os
import pyparsing
-from registry import TagRegistry
+from .registry import TagRegistry
from capa.chem import chemcalc
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
#########################################################################
@@ -366,6 +366,12 @@ class ChoiceGroup(InputTypeBase):
self.choices = self.extract_choices(self.xml)
+ @classmethod
+ def get_attributes(cls):
+ return [Attribute("show_correctness", "always"),
+ Attribute("submitted_message", "Answer received.")]
+
+
def _extra_context(self):
return {'input_type': self.html_input_type,
'choices': self.choices,
@@ -851,6 +857,10 @@ class DragAndDropInput(InputTypeBase):
if tag_type == 'draggable' and not self.no_labels:
dic['label'] = dic['label'] or dic['id']
+ if tag_type == 'draggable':
+ dic['target_fields'] = [parse(target, 'target') for target in
+ tag.iterchildren('target')]
+
return dic
# add labels to images?:
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index d49d030df5..6bf98999d8 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -28,15 +28,15 @@ from collections import namedtuple
from shapely.geometry import Point, MultiPoint
# specific library imports
-from calc import evaluator, UndefinedVariable
-from correctmap import CorrectMap
+from .calc import evaluator, UndefinedVariable
+from .correctmap import CorrectMap
from datetime import datetime
-from util import *
+from .util import *
from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
import xqueue_interface
-log = logging.getLogger('mitx.' + __name__)
+log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
@@ -231,16 +231,14 @@ class LoncapaResponse(object):
# hint specified by function?
hintfn = hintgroup.get('hintfn')
if hintfn:
- '''
- Hint is determined by a function defined in the
'''
- snippets = [{'snippet': """
+ snippets = [{'snippet': r"""
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
@@ -1104,7 +1102,7 @@ def sympy_check2():
# the form:
# {'overall_message': STRING,
# 'input_list': [{ 'ok': BOOLEAN, 'msg': STRING }, ...] }
- #
+ #
# This allows the function to return an 'overall message'
# that applies to the entire problem, as well as correct/incorrect
# status and messages for individual inputs
@@ -1197,7 +1195,7 @@ class SymbolicResponse(CustomResponse):
"""
Symbolic math response checking, using symmath library.
"""
- snippets = [{'snippet': '''
+ snippets = [{'snippet': r'''Compute \[ \exp\left(-i \frac{\theta}{2} \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) \]
and give the resulting \(2\times 2\) matrix:
@@ -1988,7 +1986,7 @@ class AnnotationResponse(LoncapaResponse):
for inputfield in self.inputfields:
option_scoring = dict([(option['id'], {
- 'correctness': choices.get(option['choice']),
+ 'correctness': choices.get(option['choice']),
'points': scoring.get(option['choice'])
}) for option in self._find_options(inputfield) ])
diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html
index e4a3f1dc39..e1ff40b6a1 100644
--- a/common/lib/capa/capa/templates/choicegroup.html
+++ b/common/lib/capa/capa/templates/choicegroup.html
@@ -1,7 +1,7 @@
<%
@@ -226,7 +226,7 @@
%>
% if testcenter_exam_info is not None:
- % if registration is None and testcenter_exam_info.is_registering():
+ % if registration is None and testcenter_exam_info.is_registering():
% endif
- % if not registration.is_accepted and not registration.is_rejected:
+ % if not registration.is_accepted and not registration.is_rejected:
Your registration for the Pearson exam is pending. Within a few days, you should see a confirmation number here, which can be used to schedule your exam.