- Course Staff
+
+ Staff Member #1
-Biography of instructor/staff member #1
-Staff Member #1
+Biography of instructor/staff member #1
+diff --git a/.tx/config b/.tx/config
index 540c4732af..9288418924 100644
--- a/.tx/config
+++ b/.tx/config
@@ -1,25 +1,25 @@
[main]
host = https://www.transifex.com
-[edx-studio.django-partial]
+[edx-platform.django-partial]
file_filter = conf/locale/ Enter your (optional) instructions for the exercise in HTML format. Annotations are specified by an Add your HTML with annotation spans here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla facilisi. Why is the sky blue? Why is the sky blue? This is where you can add additional pages to your courseware. Click the 'edit' button to begin editing. This is where you can add additional information about your course. Include your long course description here. The long course description should contain 150-400 words. Include your long course description here. The long course description should contain 150-400 words. This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags. This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags. Add information about course prerequisites here. Add information about course prerequisites here. Biography of instructor/staff member #1 Biography of instructor/staff member #1 Biography of instructor/staff member #2 Biography of instructor/staff member #2 No, a free online version of Chemistry: Principles, Patterns, and Applications, First Edition by Bruce Averill and Patricia Eldredge will be available, though you can purchase a printed version (published by FlatWorld Knowledge) if you’d like. No, a free online version of Chemistry: Principles, Patterns, and Applications, First Edition by Bruce Averill and Patricia Eldredge will be available, though you can purchase a printed version (published by FlatWorld Knowledge) if you’d like. Your answer would be displayed here. Your answer would be displayed here. Enter your (optional) instructions for the exercise in HTML format. Annotations are specified by an Add your HTML with annotation spans here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla facilisi. Why is the sky blue? This is a paragraph. It will take care of line breaks for you. HTML only parses the location
-
- of tags for inserting line breaks into your doc, not
- line
- breaks
- you
- add
- yourself.
- You can refer to other parts of the internet with a link, to other parts of your course by prepending your link with /course/ Now a list: This list has an ordering Note, we have a lot of standard edX styles, so please try to avoid any custom styling, and make sure that you make a note of any custom styling that you do yourself so that we can incorporate it into
- tools that other people can use. Make a high pass filter Explanation A voltage divider that evenly divides the input voltage can be formed with two identically valued resistors, with the sampled voltage taken in between the two. A simple high-pass filter without any further constaints can be formed by simply putting a resister in series with a capacitor. The actual values of the components do not really matter in order to meet the constraints of the problem. Make a high pass filter Explanation A voltage divider that evenly divides the input voltage can be formed with two identically valued resistors, with the sampled voltage taken in between the two. A simple high-pass filter without any further constaints can be formed by simply putting a resister in series with a capacitor. The actual values of the components do not really matter in order to meet the constraints of the problem.
- A custom python-evaluated input problem accepts one or more lines of text input from the
- student, and evaluates the inputs for correctness based on evaluation using a
- python script embedded within the problem.
-
+ A custom python-evaluated input problem accepts one or more lines of text input from the
+ student, and evaluates the inputs for correctness based on evaluation using a
+ python script embedded within the problem.
+ Enter two integers which sum to 10: Enter two integers which sum to 10: Enter two integers which sum to 20: Explanation Any set of integers on the line \(y = 10 - x\) and \(y = 20 - x\) satisfy these constraints. Enter two integers which sum to 20: Explanation Any set of integers on the line \(y = 10 - x\) and \(y = 20 - x\) satisfy these constraints.
@@ -43,5 +42,3 @@ data: |
diff --git a/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml b/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml
index 82d7e8c1ae..097055cfe3 100644
--- a/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml
@@ -85,6 +85,7 @@ metadata:
can contain equations: $\alpha = \frac{2}{\sqrt{1+\gamma}}$ }
This is some text after the showhide example.
+ markdown: !!null
data: |
@@ -214,4 +215,3 @@ data: |
@@ -54,4 +44,3 @@ data: |
@@ -83,5 +71,3 @@ data: |
Dropdown problems give a limited set of options for students to respond with, and present those options
@@ -45,4 +39,3 @@ data: |
[
"} "metadata" : ignored}
location_base = course_updates.location.url()
diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
index cb24af47e0..756adad7c4 100644
--- a/cms/djangoapps/contentstore/features/common.py
+++ b/cms/djangoapps/contentstore/features/common.py
@@ -208,7 +208,7 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
def i_created_a_video_component(step):
world.create_component_instance(
step, '.large-video-icon',
- 'i4x://edx/templates/video/default',
+ 'video',
'.xmodule_VideoModule'
)
diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
index 43164f62be..2f1788c6a4 100644
--- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
+++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
@@ -7,9 +7,9 @@ from terrain.steps import reload_the_page
@world.absorb
-def create_component_instance(step, component_button_css, instance_id, expected_css):
+def create_component_instance(step, component_button_css, category, expected_css, boilerplate=None):
click_new_component_button(step, component_button_css)
- click_component_from_menu(instance_id, expected_css)
+ click_component_from_menu(category, boilerplate, expected_css)
@world.absorb
@@ -19,7 +19,7 @@ def click_new_component_button(step, component_button_css):
@world.absorb
-def click_component_from_menu(instance_id, expected_css):
+def click_component_from_menu(category, boilerplate, expected_css):
"""
Creates a component from `instance_id`. For components with more
than one template, clicks on `elem_css` to create the new
@@ -27,11 +27,13 @@ def click_component_from_menu(instance_id, expected_css):
as the user clicks the appropriate button, so we assert that the
expected component is present.
"""
- elem_css = "a[data-location='%s']" % instance_id
+ if boilerplate:
+ elem_css = "a[data-category='{}'][data-boilerplate='{}']".format(category, boilerplate)
+ else:
+ elem_css = "a[data-category='{}']:not([data-boilerplate])".format(category)
elements = world.css_find(elem_css)
- assert(len(elements) == 1)
- if elements[0]['id'] == instance_id: # If this is a component with multiple templates
- world.css_click(elem_css)
+ assert_equal(len(elements), 1)
+ world.css_click(elem_css)
assert_equal(1, len(world.css_find(expected_css)))
diff --git a/cms/djangoapps/contentstore/features/discussion-editor.py b/cms/djangoapps/contentstore/features/discussion-editor.py
index ae3da3c458..8e4becb62e 100644
--- a/cms/djangoapps/contentstore/features/discussion-editor.py
+++ b/cms/djangoapps/contentstore/features/discussion-editor.py
@@ -8,7 +8,7 @@ from lettuce import world, step
def i_created_discussion_tag(step):
world.create_component_instance(
step, '.large-discussion-icon',
- 'i4x://edx/templates/discussion/Discussion_Tag',
+ 'discussion',
'.xmodule_DiscussionModule'
)
@@ -17,14 +17,14 @@ def i_created_discussion_tag(step):
def i_see_only_the_settings_and_values(step):
world.verify_all_setting_entries(
[
- ['Category', "Week 1", True],
- ['Display Name', "Discussion Tag", True],
- ['Subcategory', "Topic-Level Student-Visible Label", True]
+ ['Category', "Week 1", False],
+ ['Display Name', "Discussion Tag", False],
+ ['Subcategory', "Topic-Level Student-Visible Label", False]
])
@step('creating a discussion takes a single click')
def discussion_takes_a_single_click(step):
assert(not world.is_css_present('.xmodule_DiscussionModule'))
- world.css_click("a[data-location='i4x://edx/templates/discussion/Discussion_Tag']")
+ world.css_click("a[data-category='discussion']")
assert(world.is_css_present('.xmodule_DiscussionModule'))
diff --git a/cms/djangoapps/contentstore/features/html-editor.py b/cms/djangoapps/contentstore/features/html-editor.py
index 054c0ea642..b03388c89a 100644
--- a/cms/djangoapps/contentstore/features/html-editor.py
+++ b/cms/djangoapps/contentstore/features/html-editor.py
@@ -7,11 +7,11 @@ from lettuce import world, step
@step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step):
world.create_component_instance(
- step, '.large-html-icon', 'i4x://edx/templates/html/Blank_HTML_Page',
+ step, '.large-html-icon', 'html',
'.xmodule_HtmlModule'
)
@step('I see only the HTML display name setting$')
def i_see_only_the_html_display_name(step):
- world.verify_all_setting_entries([['Display Name', "Blank HTML Page", True]])
+ world.verify_all_setting_entries([['Display Name', "Blank HTML Page", False]])
diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py
index 5d12b23d90..64b2ec9b5c 100644
--- a/cms/djangoapps/contentstore/features/problem-editor.py
+++ b/cms/djangoapps/contentstore/features/problem-editor.py
@@ -18,8 +18,9 @@ def i_created_blank_common_problem(step):
world.create_component_instance(
step,
'.large-problem-icon',
- 'i4x://edx/templates/problem/Blank_Common_Problem',
- '.xmodule_CapaModule'
+ 'problem',
+ '.xmodule_CapaModule',
+ 'blank_common.yaml'
)
@@ -35,8 +36,8 @@ def i_see_five_settings_with_values(step):
[DISPLAY_NAME, "Blank Common Problem", True],
[MAXIMUM_ATTEMPTS, "", False],
[PROBLEM_WEIGHT, "", False],
- [RANDOMIZATION, "Never", True],
- [SHOW_ANSWER, "Finished", True]
+ [RANDOMIZATION, "Never", False],
+ [SHOW_ANSWER, "Finished", False]
])
@@ -94,7 +95,7 @@ def my_change_to_randomization_is_persisted(step):
def i_can_revert_to_default_for_randomization(step):
world.revert_setting_entry(RANDOMIZATION)
world.save_component_and_reopen(step)
- world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False)
+ world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Never", False)
@step('I can set the weight to "(.*)"?')
@@ -156,7 +157,7 @@ def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon')
# Go to advanced tab.
world.css_click('#ui-id-2')
- world.click_component_from_menu("i4x://edx/templates/problem/Problem_Written_in_LaTeX", '.xmodule_CapaModule')
+ world.click_component_from_menu("problem", "latex_problem.yaml", '.xmodule_CapaModule')
@step('I edit and compile the High Level Source')
@@ -203,7 +204,7 @@ def verify_modified_display_name_with_special_chars():
def verify_unset_display_name():
- world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False)
+ world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'Blank Advanced Problem', False)
def set_weight(weight):
diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
index 9ab17fbdac..41e39513ea 100644
--- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
+++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
@@ -22,7 +22,7 @@ def have_a_course_with_1_section(step):
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
parent_location=section.location,
- template='i4x://edx/templates/sequential/Empty',
+ category='sequential',
display_name='Subsection One',)
@@ -33,18 +33,18 @@ def have_a_course_with_two_sections(step):
section = world.ItemFactory.create(parent_location=course.location)
subsection1 = world.ItemFactory.create(
parent_location=section.location,
- template='i4x://edx/templates/sequential/Empty',
+ category='sequential',
display_name='Subsection One',)
section2 = world.ItemFactory.create(
parent_location=course.location,
display_name='Section Two',)
subsection2 = world.ItemFactory.create(
parent_location=section2.location,
- template='i4x://edx/templates/sequential/Empty',
+ category='sequential',
display_name='Subsection Alpha',)
subsection3 = world.ItemFactory.create(
parent_location=section2.location,
- template='i4x://edx/templates/sequential/Empty',
+ category='sequential',
display_name='Subsection Beta',)
diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py
index a6865fdd6d..e0f76b30ad 100644
--- a/cms/djangoapps/contentstore/features/video-editor.py
+++ b/cms/djangoapps/contentstore/features/video-editor.py
@@ -7,7 +7,7 @@ from lettuce import world, step
@step('I see the correct settings and default values$')
def i_see_the_correct_settings_and_values(step):
world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False],
- ['Display Name', 'default', True],
+ ['Display Name', 'Video Title', False],
['Download Track', '', False],
['Download Video', '', False],
['Show Captions', 'True', False],
diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py
index cb59193f17..a6a362befc 100644
--- a/cms/djangoapps/contentstore/features/video.py
+++ b/cms/djangoapps/contentstore/features/video.py
@@ -14,7 +14,7 @@ def does_not_autoplay(_step):
@step('creating a video takes a single click')
def video_takes_a_single_click(_step):
assert(not world.is_css_present('.xmodule_VideoModule'))
- world.css_click("a[data-location='i4x://edx/templates/video/default']")
+ world.css_click("a[data-category='video']")
assert(world.is_css_present('.xmodule_VideoModule'))
diff --git a/cms/djangoapps/contentstore/management/commands/update_templates.py b/cms/djangoapps/contentstore/management/commands/update_templates.py
deleted file mode 100644
index 36348314b9..0000000000
--- a/cms/djangoapps/contentstore/management/commands/update_templates.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from xmodule.templates import update_templates
-from xmodule.modulestore.django import modulestore
-from django.core.management.base import BaseCommand
-
-
-class Command(BaseCommand):
- help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
-
- def handle(self, *args, **options):
- update_templates(modulestore('direct'))
diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py
index 726d4bb0ce..bce4b0326c 100644
--- a/cms/djangoapps/contentstore/module_info_model.py
+++ b/cms/djangoapps/contentstore/module_info_model.py
@@ -3,13 +3,13 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
-def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
+def get_module_info(store, location, rewrite_static_links=False):
try:
module = store.get_item(location)
except ItemNotFoundError:
# create a new one
- template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
- module = store.clone_item(template_location, location)
+ store.create_and_save_xmodule(location)
+ module = store.get_item(location)
data = module.data
if rewrite_static_links:
@@ -29,7 +29,8 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
'id': module.location.url(),
'data': data,
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
- 'metadata': module._model_data._kvs._metadata
+ # what's the intent here? all metadata incl inherited & namespaced?
+ 'metadata': module.xblock_kvs._metadata
}
@@ -37,14 +38,11 @@ def set_module_info(store, location, post_data):
module = None
try:
module = store.get_item(location)
- except:
- pass
-
- if module is None:
- # new module at this location
- # presume that we have an 'Empty' template
- template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
- module = store.clone_item(template_location, location)
+ except ItemNotFoundError:
+ # new module at this location: almost always used for the course about pages; thus, no parent. (there
+ # are quite a handful of about page types available for a course and only the overview is pre-created)
+ store.create_and_save_xmodule(location)
+ module = store.get_item(location)
if post_data.get('data') is not None:
data = post_data['data']
@@ -79,4 +77,4 @@ def set_module_info(store, location, post_data):
# commit to datastore
# 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)
+ store.update_metadata(location, module.xblock_kvs._metadata)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index be122fa1a4..f67af41cdc 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -24,12 +24,11 @@ from auth.authz import add_user_to_creator_group
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
-from xmodule.modulestore import Location
+from xmodule.modulestore import Location, mongo
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore, _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, perform_xlint
from xmodule.modulestore.inheritance import own_metadata
@@ -135,7 +134,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Video Alpha',
'Word cloud',
'Annotation',
- 'Open Ended Response',
+ 'Open Ended Grading',
'Peer Grading Interface'])
def test_advanced_components_require_two_clicks(self):
@@ -183,7 +182,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
- draft_store.clone_item(html_module.location, html_module.location)
+ draft_store.convert_to_draft(html_module.location)
# now query get_items() to get this location with revision=None, this should just
# return back a single item (not 2)
@@ -215,7 +214,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod)
self.assertNotIn('graceperiod', own_metadata(html_module))
- draft_store.clone_item(html_module.location, html_module.location)
+ draft_store.convert_to_draft(html_module.location)
# refetch to check metadata
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
@@ -233,7 +232,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertNotIn('graceperiod', own_metadata(html_module))
# put back in draft and change metadata and see if it's now marked as 'own_metadata'
- draft_store.clone_item(html_module.location, html_module.location)
+ draft_store.convert_to_draft(html_module.location)
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
new_graceperiod = timedelta(hours=1)
@@ -255,7 +254,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
draft_store.publish(html_module.location, 0)
# and re-read and verify 'own-metadata'
- draft_store.clone_item(html_module.location, html_module.location)
+ draft_store.convert_to_draft(html_module.location)
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertIn('graceperiod', own_metadata(html_module))
@@ -278,7 +277,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
)
# put into draft
- modulestore('draft').clone_item(problem.location, problem.location)
+ modulestore('draft').convert_to_draft(problem.location)
# make sure we can query that item and verify that it is a draft
draft_problem = modulestore('draft').get_item(
@@ -309,12 +308,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
- ItemFactory.create(parent_location=course_location,
- template="i4x://edx/templates/static_tab/Empty",
- display_name="Static_1")
- ItemFactory.create(parent_location=course_location,
- template="i4x://edx/templates/static_tab/Empty",
- display_name="Static_2")
+ ItemFactory.create(
+ parent_location=course_location,
+ category="static_tab",
+ display_name="Static_1")
+ ItemFactory.create(
+ parent_location=course_location,
+ category="static_tab",
+ display_name="Static_2")
course = module_store.get_item(Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None]))
@@ -371,7 +372,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
chapterloc = ItemFactory.create(parent_location=course_location, display_name="Chapter").location
- ItemFactory.create(parent_location=chapterloc, template='i4x://edx/templates/sequential/Empty', display_name="Sequential")
+ ItemFactory.create(parent_location=chapterloc, category='sequential', display_name="Sequential")
sequential = direct_store.get_item(Location(['i4x', 'edX', '999', 'sequential', 'Sequential', None]))
chapter = direct_store.get_item(Location(['i4x', 'edX', '999', 'chapter', 'Chapter', None]))
@@ -574,7 +575,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_clone_course(self):
course_data = {
- 'template': 'i4x://edx/templates/course/Empty',
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
@@ -614,10 +614,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
location = Location('i4x://MITx/999/chapter/neuvo')
- self.assertRaises(InvalidVersionError, draft_store.clone_item, 'i4x://edx/templates/chapter/Empty',
- location)
- direct_store.clone_item('i4x://edx/templates/chapter/Empty', location)
- self.assertRaises(InvalidVersionError, draft_store.clone_item, location, location)
+ # Ensure draft mongo store does not allow us to create chapters either directly or via convert to draft
+ self.assertRaises(InvalidVersionError, draft_store.create_and_save_xmodule, location)
+ direct_store.create_and_save_xmodule(location)
+ self.assertRaises(InvalidVersionError, draft_store.convert_to_draft, location)
self.assertRaises(InvalidVersionError, draft_store.update_item, location, 'chapter data')
@@ -652,9 +652,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
vertical = module_store.get_item(Location(['i4x', 'edX', 'toy',
'vertical', 'vertical_test', None]), depth=1)
- draft_store.clone_item(vertical.location, vertical.location)
+ draft_store.convert_to_draft(vertical.location)
for child in vertical.get_children():
- draft_store.clone_item(child.location, child.location)
+ draft_store.convert_to_draft(child.location)
# delete the course
delete_course(module_store, content_store, location, commit=True)
@@ -687,26 +687,35 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(module_store, 'common/test/data/', ['toy'])
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
- # get a vertical (and components in it) to put into 'draft'
- vertical = module_store.get_item(Location(['i4x', 'edX', 'toy',
- 'vertical', 'vertical_test', None]), depth=1)
-
- draft_store.clone_item(vertical.location, vertical.location)
-
+ # get a vertical (and components in it) to copy into an orphan sub dag
+ vertical = module_store.get_item(
+ Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None]),
+ depth=1
+ )
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
- draft_store.clone_item(vertical.location, Location(['i4x', 'edX', 'toy',
- 'vertical', 'no_references', 'draft']))
+ vertical.location = mongo.draft.as_draft(vertical.location.replace(name='no_references'))
+ draft_store.save_xmodule(vertical)
+ orphan_vertical = draft_store.get_item(vertical.location)
+ self.assertEqual(orphan_vertical.location.name, 'no_references')
+ # get the original vertical (and components in it) to put into 'draft'
+ vertical = module_store.get_item(
+ Location(['i4x', 'edX', 'toy', 'vertical', 'vertical_test', None]),
+ depth=1)
+ self.assertEqual(len(orphan_vertical.children), len(vertical.children))
+ draft_store.convert_to_draft(vertical.location)
for child in vertical.get_children():
- draft_store.clone_item(child.location, child.location)
+ draft_store.convert_to_draft(child.location)
root_dir = path(mkdtemp_clean())
- # now create a private vertical
- private_vertical = draft_store.clone_item(vertical.location,
- Location(['i4x', 'edX', 'toy', 'vertical', 'a_private_vertical', None]))
+ # now create a new/different private (draft only) vertical
+ vertical.location = mongo.draft.as_draft(Location(['i4x', 'edX', 'toy', 'vertical', 'a_private_vertical', None]))
+ draft_store.save_xmodule(vertical)
+ private_vertical = draft_store.get_item(vertical.location)
+ vertical = None # blank out b/c i destructively manipulated its location 2 lines above
- # add private to list of children
+ # add the new private to list of children
sequential = module_store.get_item(Location(['i4x', 'edX', 'toy',
'sequential', 'vertical_sequential', None]))
private_location_no_draft = private_vertical.location.replace(revision=None)
@@ -885,7 +894,6 @@ class ContentStoreTest(ModuleStoreTestCase):
self.client.login(username=uname, password=password)
self.course_data = {
- 'template': 'i4x://edx/templates/course/Empty',
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
@@ -1029,17 +1037,17 @@ class ContentStoreTest(ModuleStoreTestCase):
html=True
)
- def test_clone_item(self):
+ def test_create_item(self):
"""Test cloning an item. E.g. creating a new section"""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
section_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
- 'template': 'i4x://edx/templates/chapter/Empty',
+ 'category': 'chapter',
'display_name': 'Section One',
}
- resp = self.client.post(reverse('clone_item'), section_data)
+ resp = self.client.post(reverse('create_item'), section_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
@@ -1054,14 +1062,14 @@ class ContentStoreTest(ModuleStoreTestCase):
problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
- 'template': 'i4x://edx/templates/problem/Blank_Common_Problem'
+ 'category': 'problem'
}
- resp = self.client.post(reverse('clone_item'), problem_data)
+ resp = self.client.post(reverse('create_item'), problem_data)
self.assertEqual(resp.status_code, 200)
payload = parse_json(resp)
- problem_loc = payload['id']
+ problem_loc = Location(payload['id'])
problem = get_modulestore(problem_loc).get_item(problem_loc)
# should be a CapaDescriptor
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
@@ -1194,10 +1202,9 @@ class ContentStoreTest(ModuleStoreTestCase):
CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
new_component_location = Location('i4x', 'edX', '999', 'discussion', 'new_component')
- source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag')
# crate a new module and add it as a child to a vertical
- module_store.clone_item(source_template_location, new_component_location)
+ module_store.create_and_save_xmodule(new_component_location)
new_discussion_item = module_store.get_item(new_component_location)
@@ -1218,10 +1225,9 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.modulestore_update_signal.connect(_signal_hander)
new_component_location = Location('i4x', 'edX', '999', 'html', 'new_component')
- source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
# crate a new module
- module_store.clone_item(source_template_location, new_component_location)
+ module_store.create_and_save_xmodule(new_component_location)
finally:
module_store.modulestore_update_signal = None
@@ -1239,14 +1245,14 @@ class ContentStoreTest(ModuleStoreTestCase):
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
+ self.assertEqual(course.start, vertical.lms.start)
self.assertGreater(len(verticals), 0)
new_component_location = Location('i4x', 'edX', 'toy', '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
- module_store.clone_item(source_template_location, new_component_location)
+ module_store.create_and_save_xmodule(new_component_location)
parent = verticals[0]
module_store.update_children(parent.location, parent.children + [new_component_location.url()])
@@ -1256,6 +1262,8 @@ class ContentStoreTest(ModuleStoreTestCase):
# check for grace period definition which should be defined at the course level
self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
+ self.assertEqual(parent.lms.start, new_module.lms.start)
+ self.assertEqual(course.start, new_module.lms.start)
self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)
@@ -1271,29 +1279,25 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(timedelta(1), new_module.lms.graceperiod)
+ def test_default_metadata_inheritance(self):
+ course = CourseFactory.create()
+ vertical = ItemFactory.create(parent_location=course.location)
+ course.children.append(vertical)
+ # in memory
+ self.assertIsNotNone(course.start)
+ self.assertEqual(course.start, vertical.lms.start)
+ self.assertEqual(course.textbooks, [])
+ self.assertIn('GRADER', course.grading_policy)
+ self.assertIn('GRADE_CUTOFFS', course.grading_policy)
+ self.assertGreaterEqual(len(course.checklists), 4)
-class TemplateTestCase(ModuleStoreTestCase):
-
- def test_template_cleanup(self):
+ # by fetching
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')
-
- 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
- update_templates(modulestore('direct'))
-
- # now try to find dangling template, it should not be in DB any longer
- asserted = False
- try:
- verify_create = module_store.get_item(bogus_template_location)
- except ItemNotFoundError:
- asserted = True
-
- self.assertTrue(asserted)
+ fetched_course = module_store.get_item(course.location)
+ fetched_item = module_store.get_item(vertical.location)
+ self.assertIsNotNone(fetched_course.start)
+ self.assertEqual(course.start, fetched_course.start)
+ self.assertEqual(fetched_course.start, fetched_item.lms.start)
+ self.assertEqual(course.textbooks, fetched_course.textbooks)
+ # is this test too strict? i.e., it requires the dicts to be ==
+ self.assertEqual(course.checklists, fetched_course.checklists)
diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index 21d7d69d41..44eb16436d 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -19,6 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
+from xmodule.modulestore.django import modulestore
from xmodule.fields import Date
from .utils import CourseTestCase
@@ -36,7 +37,6 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start))
self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end))
self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus))
- self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview)
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
@@ -49,7 +49,6 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ")
self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ")
self.assertIsNone(jsondetails['syllabus'], "syllabus somehow initialized")
- self.assertEqual(jsondetails['overview'], "", "overview somehow initialized")
self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized")
self.assertIsNone(jsondetails['effort'], "effort somehow initialized")
@@ -352,7 +351,7 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness
- self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
+ self.assertEqual('finished', 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_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py
index a292b7316e..88df19ec2d 100644
--- a/cms/djangoapps/contentstore/tests/test_i18n.py
+++ b/cms/djangoapps/contentstore/tests/test_i18n.py
@@ -35,7 +35,6 @@ class InternationalizationTest(ModuleStoreTestCase):
self.user.save()
self.course_data = {
- 'template': 'i4x://edx/templates/course/Empty',
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py
index 4e6c951d9b..cd203e6af7 100644
--- a/cms/djangoapps/contentstore/tests/test_item.py
+++ b/cms/djangoapps/contentstore/tests/test_item.py
@@ -1,6 +1,9 @@
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse
+from xmodule.capa_module import CapaDescriptor
+import json
+from xmodule.modulestore.django import modulestore
class DeleteItem(CourseTestCase):
@@ -11,14 +14,199 @@ class DeleteItem(CourseTestCase):
def testDeleteStaticPage(self):
# Add static tab
- data = {
+ data = json.dumps({
'parent_location': 'i4x://mitX/333/course/Dummy_Course',
- 'template': 'i4x://edx/templates/static_tab/Empty'
- }
+ 'category': 'static_tab'
+ })
- resp = self.client.post(reverse('clone_item'), data)
+ resp = self.client.post(reverse('create_item'), data,
+ content_type="application/json")
self.assertEqual(resp.status_code, 200)
# Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
resp = self.client.post(reverse('delete_item'), resp.content, "application/json")
self.assertEqual(resp.status_code, 200)
+
+
+class TestCreateItem(CourseTestCase):
+ """
+ Test the create_item handler thoroughly
+ """
+ def response_id(self, response):
+ """
+ Get the id from the response payload
+ :param response:
+ """
+ parsed = json.loads(response.content)
+ return parsed['id']
+
+ def test_create_nicely(self):
+ """
+ Try the straightforward use cases
+ """
+ # create a chapter
+ display_name = 'Nicely created'
+ resp = self.client.post(
+ reverse('create_item'),
+ json.dumps({
+ 'parent_location': self.course.location.url(),
+ 'display_name': display_name,
+ 'category': 'chapter'
+ }),
+ content_type="application/json"
+ )
+ self.assertEqual(resp.status_code, 200)
+
+ # get the new item and check its category and display_name
+ chap_location = self.response_id(resp)
+ new_obj = modulestore().get_item(chap_location)
+ self.assertEqual(new_obj.category, 'chapter')
+ self.assertEqual(new_obj.display_name, display_name)
+ self.assertEqual(new_obj.location.org, self.course.location.org)
+ self.assertEqual(new_obj.location.course, self.course.location.course)
+
+ # get the course and ensure it now points to this one
+ course = modulestore().get_item(self.course.location)
+ self.assertIn(chap_location, course.children)
+
+ # use default display name
+ resp = self.client.post(
+ reverse('create_item'),
+ json.dumps({
+ 'parent_location': chap_location,
+ 'category': 'vertical'
+ }),
+ content_type="application/json"
+ )
+ self.assertEqual(resp.status_code, 200)
+
+ vert_location = self.response_id(resp)
+
+ # create problem w/ boilerplate
+ template_id = 'multiplechoice.yaml'
+ resp = self.client.post(
+ reverse('create_item'),
+ json.dumps({
+ 'parent_location': vert_location,
+ 'category': 'problem',
+ 'boilerplate': template_id
+ }),
+ content_type="application/json"
+ )
+ self.assertEqual(resp.status_code, 200)
+ prob_location = self.response_id(resp)
+ problem = modulestore('draft').get_item(prob_location)
+ # ensure it's draft
+ self.assertTrue(problem.is_draft)
+ # check against the template
+ template = CapaDescriptor.get_template(template_id)
+ self.assertEqual(problem.data, template['data'])
+ self.assertEqual(problem.display_name, template['metadata']['display_name'])
+ self.assertEqual(problem.markdown, template['metadata']['markdown'])
+
+ def test_create_item_negative(self):
+ """
+ Negative tests for create_item
+ """
+ # non-existent boilerplate: creates a default
+ resp = self.client.post(
+ reverse('create_item'),
+ json.dumps(
+ {'parent_location': self.course.location.url(),
+ 'category': 'problem',
+ 'boilerplate': 'nosuchboilerplate.yaml'
+ }),
+ content_type="application/json"
+ )
+ self.assertEqual(resp.status_code, 200)
+
+class TestEditItem(CourseTestCase):
+ """
+ Test contentstore.views.item.save_item
+ """
+ def response_id(self, response):
+ """
+ Get the id from the response payload
+ :param response:
+ """
+ parsed = json.loads(response.content)
+ return parsed['id']
+
+ def setUp(self):
+ """ Creates the test course structure and a couple problems to 'edit'. """
+ super(TestEditItem, self).setUp()
+ # create a chapter
+ display_name = 'chapter created'
+ resp = self.client.post(
+ reverse('create_item'),
+ json.dumps(
+ {'parent_location': self.course.location.url(),
+ 'display_name': display_name,
+ 'category': 'chapter'
+ }),
+ content_type="application/json"
+ )
+ chap_location = self.response_id(resp)
+ resp = self.client.post(
+ reverse('create_item'),
+ json.dumps(
+ {'parent_location': chap_location,
+ 'category': 'vertical'
+ }),
+ content_type="application/json"
+ )
+ vert_location = self.response_id(resp)
+ # create problem w/ boilerplate
+ template_id = 'multiplechoice.yaml'
+ resp = self.client.post(
+ reverse('create_item'),
+ json.dumps({'parent_location': vert_location,
+ 'category': 'problem',
+ 'boilerplate': template_id
+ }),
+ content_type="application/json"
+ )
+ self.problems = [self.response_id(resp)]
+
+ def test_delete_field(self):
+ """
+ Sending null in for a field 'deletes' it
+ """
+ self.client.post(
+ reverse('save_item'),
+ json.dumps({
+ 'id': self.problems[0],
+ 'metadata': {'rerandomize': 'onreset'}
+ }),
+ content_type="application/json"
+ )
+ problem = modulestore('draft').get_item(self.problems[0])
+ self.assertEqual(problem.rerandomize, 'onreset')
+ self.client.post(
+ reverse('save_item'),
+ json.dumps({
+ 'id': self.problems[0],
+ 'metadata': {'rerandomize': None}
+ }),
+ content_type="application/json"
+ )
+ problem = modulestore('draft').get_item(self.problems[0])
+ self.assertEqual(problem.rerandomize, 'never')
+
+
+ def test_null_field(self):
+ """
+ Sending null in for a field 'deletes' it
+ """
+ problem = modulestore('draft').get_item(self.problems[0])
+ self.assertIsNotNone(problem.markdown)
+ self.client.post(
+ reverse('save_item'),
+ json.dumps({
+ 'id': self.problems[0],
+ 'nullout': ['markdown']
+ }),
+ content_type="application/json"
+ )
+ problem = modulestore('draft').get_item(self.problems[0])
+ self.assertIsNone(problem.markdown)
diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py
index bc9e9e8bae..a3f211a703 100644
--- a/cms/djangoapps/contentstore/tests/utils.py
+++ b/cms/djangoapps/contentstore/tests/utils.py
@@ -54,7 +54,6 @@ class CourseTestCase(ModuleStoreTestCase):
self.client.login(username=uname, password=password)
self.course = CourseFactory.create(
- template='i4x://edx/templates/course/Empty',
org='MITx',
number='999',
display_name='Robot Super Course',
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 5fa0d949b0..4973bddaca 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -19,14 +19,14 @@ NOTES_PANEL = {"name": _("My Notes"), "type": "notes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
-def get_modulestore(location):
+def get_modulestore(category_or_location):
"""
Returns the correct modulestore to use for modifying the specified location
"""
- if not isinstance(location, Location):
- location = Location(location)
+ if isinstance(category_or_location, Location):
+ category_or_location = category_or_location.category
- if location.category in DIRECT_ONLY_CATEGORIES:
+ if category_or_location in DIRECT_ONLY_CATEGORIES:
return modulestore('direct')
else:
return modulestore()
diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py
index fa0a7b7b62..fdb5857ba7 100644
--- a/cms/djangoapps/contentstore/views/checklist.py
+++ b/cms/djangoapps/contentstore/views/checklist.py
@@ -7,11 +7,11 @@ from django.views.decorators.http import require_http_methods
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
-from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
from ..utils import get_modulestore, get_url_reverse
from .access import get_location_and_verify_access
+from xmodule.course_module import CourseDescriptor
__all__ = ['get_checklists', 'update_checklist']
@@ -28,13 +28,11 @@ def get_checklists(request, org, course, name):
modulestore = get_modulestore(location)
course_module = modulestore.get_item(location)
- new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
- template_module = modulestore.get_item(new_course_template)
# If course was created before checklists were introduced, copy them over from the template.
copied = False
if not course_module.checklists:
- course_module.checklists = template_module.checklists
+ course_module.checklists = CourseDescriptor.checklists.default
copied = True
checklists, modified = expand_checklist_action_urls(course_module)
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 30958d5866..505a93903a 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -26,6 +26,8 @@ from models.settings.course_grading import CourseGradingModel
from .requests import _xmodule_recurse
from .access import has_access
+from xmodule.x_module import XModuleDescriptor
+from xblock.plugin import PluginMissingError
__all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY',
@@ -101,7 +103,7 @@ def edit_subsection(request, location):
return render_to_response('edit_subsection.html',
{'subsection': item,
'context_course': course,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
+ 'new_unit_category': 'vertical',
'lms_link': lms_link,
'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
@@ -134,10 +136,26 @@ def edit_unit(request, location):
item = modulestore().get_item(location, depth=1)
except ItemNotFoundError:
return HttpResponseBadRequest()
-
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
component_templates = defaultdict(list)
+ for category in COMPONENT_TYPES:
+ component_class = XModuleDescriptor.load_class(category)
+ # add the default template
+ component_templates[category].append((
+ component_class.display_name.default or 'Blank',
+ category,
+ False, # No defaults have markdown (hardcoded current default)
+ None # no boilerplate for overrides
+ ))
+ # add boilerplates
+ for template in component_class.templates():
+ component_templates[category].append((
+ template['metadata'].get('display_name'),
+ category,
+ template['metadata'].get('markdown') is not None,
+ template.get('template_id')
+ ))
# 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
@@ -145,29 +163,29 @@ def edit_unit(request, location):
course_advanced_keys = course.advanced_modules
# Set component types according to course policy file
- component_types = list(COMPONENT_TYPES)
if isinstance(course_advanced_keys, list):
- course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
- if len(course_advanced_keys) > 0:
- component_types.append(ADVANCED_COMPONENT_CATEGORY)
+ for category in course_advanced_keys:
+ if category in ADVANCED_COMPONENT_TYPES:
+ # Do I need to allow for boilerplates or just defaults on the class? i.e., can an advanced
+ # have more than one entry in the menu? one for default and others for prefilled boilerplates?
+ try:
+ component_class = XModuleDescriptor.load_class(category)
+
+ component_templates['advanced'].append((
+ component_class.display_name.default or category,
+ category,
+ False,
+ None # don't override default data
+ ))
+ except PluginMissingError:
+ # dhm: I got this once but it can happen any time the course author configures
+ # an advanced component which does not exist on the server. This code here merely
+ # prevents any authors from trying to instantiate the non-existent component type
+ # by not showing it in the menu
+ pass
else:
log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
- templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
- for template in templates:
- category = template.location.category
-
- if category in course_advanced_keys:
- category = ADVANCED_COMPONENT_CATEGORY
-
- if category in component_types:
- # This is a hack to create categories for different xmodules
- component_templates[category].append((
- template.display_name_with_default,
- template.location.url(),
- hasattr(template, 'markdown') and template.markdown is not None
- ))
-
components = [
component.location.url()
for component
@@ -219,7 +237,7 @@ def edit_unit(request, location):
'subsection': containing_subsection,
'release_date': get_default_time_display(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'),
+ 'new_unit_category': 'vertical',
'unit_state': unit_state,
'published_date': get_default_time_display(item.cms.published_date) if item.cms.published_date is not None else None
})
@@ -253,7 +271,7 @@ def create_draft(request):
# This clones the existing item location to a draft location (the draft is implicit,
# because modulestore is a Draft modulestore)
- modulestore().clone_item(location, location)
+ modulestore().convert_to_draft(location)
return HttpResponse()
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index f8de053d95..0e16624c42 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -45,6 +45,7 @@ from .component import (
from django_comment_common.utils import seed_permissions_roles
import datetime
from django.utils.timezone import UTC
+from xmodule.html_module import AboutDescriptor
__all__ = ['course_index', 'create_new_course', 'course_info',
'course_info_updates', 'get_course_settings',
'course_config_graders_page',
@@ -82,10 +83,11 @@ def course_index(request, org, course, name):
'sections': sections,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
'parent_location': course.location,
- 'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
- 'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
+ 'new_section_category': 'chapter',
+ 'new_subsection_category': 'sequential',
'upload_asset_callback_url': upload_asset_callback_url,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
+ 'new_unit_category': 'vertical',
+ 'category': 'vertical'
})
@@ -98,12 +100,6 @@ def create_new_course(request):
if not is_user_in_creator_group(request.user):
raise PermissionDenied()
- # This logic is repeated in xmodule/modulestore/tests/factories.py
- # so if you change anything here, you need to also change it there.
- # TODO: write a test that creates two courses, one with the factory and
- # the other with this method, then compare them to make sure they are
- # equivalent.
- template = Location(request.POST['template'])
org = request.POST.get('org')
number = request.POST.get('number')
display_name = request.POST.get('display_name')
@@ -121,29 +117,31 @@ def create_new_course(request):
existing_course = modulestore('direct').get_item(dest_location)
except ItemNotFoundError:
pass
-
if existing_course is not None:
return JsonResponse({'ErrMsg': 'There is already a course defined with this name.'})
course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None]
courses = modulestore().get_items(course_search_location)
-
if len(courses) > 0:
return JsonResponse({'ErrMsg': 'There is already a course defined with the same organization and course number.'})
- new_course = modulestore('direct').clone_item(template, dest_location)
+ # instantiate the CourseDescriptor and then persist it
+ # note: no system to pass
+ if display_name is None:
+ metadata = {}
+ else:
+ metadata = {'display_name': display_name}
+ modulestore('direct').create_and_save_xmodule(dest_location, metadata=metadata)
+ new_course = modulestore('direct').get_item(dest_location)
- # clone a default 'about' module as well
-
- about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
- dest_about_location = dest_location._replace(category='about', name='overview')
- modulestore('direct').clone_item(about_template_location, dest_about_location)
-
- if display_name is not None:
- new_course.display_name = display_name
-
- # set a default start date to now
- new_course.start = datetime.datetime.now(UTC())
+ # clone a default 'about' overview module as well
+ dest_about_location = dest_location.replace(category='about', name='overview')
+ overview_template = AboutDescriptor.get_template('overview.yaml')
+ modulestore('direct').create_and_save_xmodule(
+ dest_about_location,
+ system=new_course.system,
+ definition_data=overview_template.get('data')
+ )
initialize_course_tabs(new_course)
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index abc5f48564..90dae10c23 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -13,16 +13,26 @@ from util.json_request import expect_json
from ..utils import get_modulestore
from .access import has_access
from .requests import _xmodule_recurse
+from xmodule.x_module import XModuleDescriptor
-__all__ = ['save_item', 'clone_item', 'delete_item']
+__all__ = ['save_item', 'create_item', 'delete_item']
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
-
@login_required
@expect_json
def save_item(request):
+ """
+ Will carry a json payload with these possible fields
+ :id (required): the id
+ :data (optional): the new value for the data
+ :metadata (optional): new values for the metadata fields.
+ Any whose values are None will be deleted not set to None! Absent ones will be left alone
+ :nullout (optional): which metadata fields to set to None
+ """
+ # The nullout is a bit of a temporary copout until we can make module_edit.coffee and the metadata editors a
+ # little smarter and able to pass something more akin to {unset: [field, field]}
item_location = request.POST['id']
# check permissions for this user within this course
@@ -42,30 +52,25 @@ def save_item(request):
children = request.POST['children']
store.update_children(item_location, children)
- # cdodge: also commit any metadata which might have been passed along in the
- # POST from the client, if it is there
- # NOTE, that the postback is not the complete metadata, as there's system metadata which is
- # not presented to the end-user for editing. So let's fetch the original and
- # 'apply' the submitted metadata, so we don't end up deleting system metadata
- if request.POST.get('metadata') is not None:
- posted_metadata = request.POST['metadata']
- # fetch original
+ # cdodge: also commit any metadata which might have been passed along
+ if request.POST.get('nullout') is not None or request.POST.get('metadata') is not None:
+ # the postback is not the complete metadata, as there's system metadata which is
+ # not presented to the end-user for editing. So let's fetch the original and
+ # 'apply' the submitted metadata, so we don't end up deleting system metadata
existing_item = modulestore().get_item(item_location)
+ for metadata_key in request.POST.get('nullout', []):
+ setattr(existing_item, metadata_key, None)
# 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, value in posted_metadata.items():
+ # IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If
+ # the intent is to make it None, use the nullout field
+ for metadata_key, value in request.POST.get('metadata', {}).items():
- if 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._model_data:
- del existing_item._model_data[metadata_key]
- del posted_metadata[metadata_key]
+ if value is None:
+ delattr(existing_item, metadata_key)
else:
- existing_item._model_data[metadata_key] = value
-
+ setattr(existing_item, metadata_key, value)
# commit to datastore
- # 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()
@@ -73,28 +78,38 @@ def save_item(request):
@login_required
@expect_json
-def clone_item(request):
+def create_item(request):
parent_location = Location(request.POST['parent_location'])
- template = Location(request.POST['template'])
+ category = request.POST['category']
display_name = request.POST.get('display_name')
if not has_access(request.user, parent_location):
raise PermissionDenied()
- parent = get_modulestore(template).get_item(parent_location)
- dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
+ parent = get_modulestore(category).get_item(parent_location)
+ dest_location = parent_location.replace(category=category, name=uuid4().hex)
- new_item = get_modulestore(template).clone_item(template, dest_location)
+ # get the metadata, display_name, and definition from the request
+ metadata = {}
+ data = None
+ template_id = request.POST.get('boilerplate')
+ if template_id is not None:
+ clz = XModuleDescriptor.load_class(category)
+ if clz is not None:
+ template = clz.get_template(template_id)
+ if template is not None:
+ metadata = template.get('metadata', {})
+ data = template.get('data')
- # replace the display name with an optional parameter passed in from the caller
if display_name is not None:
- new_item.display_name = display_name
+ metadata['display_name'] = display_name
- get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
+ get_modulestore(category).create_and_save_xmodule(dest_location, definition_data=data,
+ metadata=metadata, system=parent.system)
- if new_item.location.category not in DETACHED_CATEGORIES:
- get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
+ if category not in DETACHED_CATEGORIES:
+ get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()])
return HttpResponse(json.dumps({'id': dest_location.url()}))
diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py
index 948ed614d2..ee6b0bf84d 100644
--- a/cms/djangoapps/contentstore/views/user.py
+++ b/cms/djangoapps/contentstore/views/user.py
@@ -27,6 +27,7 @@ def index(request):
# filter out courses that we don't have access too
def course_filter(course):
return (has_access(request.user, course.location)
+ # TODO remove this condition when templates purged from db
and course.location.course != 'templates'
and course.location.org != ''
and course.location.course != ''
@@ -34,7 +35,6 @@ def index(request):
courses = filter(course_filter, courses)
return render_to_response('index.html', {
- 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.display_name,
get_url_reverse('CourseOutline', course),
get_lms_link_for_item(course.location, course_id=course.location.course_id))
diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py
index 4ea9f2f5db..e529a284c6 100644
--- a/cms/djangoapps/models/settings/course_grading.py
+++ b/cms/djangoapps/models/settings/course_grading.py
@@ -9,7 +9,7 @@ class CourseGradingModel(object):
"""
def __init__(self, course_descriptor):
self.course_location = course_descriptor.location
- self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
+ self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
self.grade_cutoffs = course_descriptor.grade_cutoffs
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
@@ -81,7 +81,7 @@ class CourseGradingModel(object):
Decode the json into CourseGradingModel and save any changes. Returns the modified model.
Probably not the usual path for updates as it's too coarse grained.
"""
- course_location = jsondict['course_location']
+ course_location = Location(jsondict['course_location'])
descriptor = get_modulestore(course_location).get_item(course_location)
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
@@ -89,7 +89,7 @@ class CourseGradingModel(object):
descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
- get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data)
+ get_modulestore(course_location).update_item(course_location, descriptor.xblock_kvs._data)
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location)
@@ -209,7 +209,7 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location)
return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
"location": location,
- "id": 99 # just an arbitrary value to
+ "id": 99 # just an arbitrary value to
}
@staticmethod
@@ -232,7 +232,7 @@ class CourseGradingModel(object):
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod
if rawgrace:
- hours_from_days = rawgrace.days*24
+ hours_from_days = rawgrace.days * 24
seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600)
hours = hours_from_days + hours_from_seconds
diff --git a/cms/static/client_templates/course_info_handouts.html b/cms/static/client_templates/course_info_handouts.html
index cf9fdb5c85..7fbbe9bc33 100644
--- a/cms/static/client_templates/course_info_handouts.html
+++ b/cms/static/client_templates/course_info_handouts.html
@@ -1,6 +1,6 @@
Edit
-date
contentCourse Handouts
+Course Handouts
<%if (model.get('data') != null) { %>
@@ -83,8 +84,9 @@
Click here to set the section name
-
- % endif
-
- % for name, location, has_markdown in templates:
- % if has_markdown or type != "problem":
-
+
-
- % for name, location, has_markdown in templates:
- % if not has_markdown:
-
-
+ % for name, category, has_markdown, boilerplate_name in sorted(templates):
+ % if has_markdown or type != "problem":
+ % if boilerplate_name is None:
+
+
+ % for name, category, has_markdown, boilerplate_name in sorted(templates):
+ % if not has_markdown:
+
+ <annotation> tag which may may have the following attributes:
+
+ title (optional). Title of the annotation. Defaults to Commentary if omitted.body (required). Text of the annotation.problem (optional). Numeric index of the problem associated with this annotation. This is a zero-based index, so the first problem on the page would have problem="0".highlight (optional). Possible values: yellow, red, orange, green, blue, or purple. Defaults to yellow if this attribute is omitted.",
+ scope=Scope.content
+ )
+
+
+class CourseInfoModule(CourseInfoFields, HtmlModule):
+ """
+ Just to support xblock field overrides
+ """
+ pass
+
+
+class CourseInfoDescriptor(CourseInfoFields, HtmlDescriptor):
"""
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
in order to be able to create new ones
"""
- template_dir_name = "courseinfo"
+ template_dir_name = None
+ module_class = CourseInfoModule
diff --git a/common/lib/xmodule/xmodule/js/spec/combinedopenended/edit_spec.coffee b/common/lib/xmodule/xmodule/js/spec/combinedopenended/edit_spec.coffee
index aa077da450..d859a59dda 100644
--- a/common/lib/xmodule/xmodule/js/spec/combinedopenended/edit_spec.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/combinedopenended/edit_spec.coffee
@@ -11,13 +11,13 @@ describe 'OpenEndedMarkdownEditingDescriptor', ->
@descriptor = new OpenEndedMarkdownEditingDescriptor($('.combinedopenended-editor'))
@descriptor.createXMLEditor('replace with markdown')
saveResult = @descriptor.save()
- expect(saveResult.metadata.markdown).toEqual(null)
+ expect(saveResult.nullout).toEqual(['markdown'])
expect(saveResult.data).toEqual('replace with markdown')
it 'saves xml from the xml editor', ->
loadFixtures 'combinedopenended-without-markdown.html'
@descriptor = new OpenEndedMarkdownEditingDescriptor($('.combinedopenended-editor'))
saveResult = @descriptor.save()
- expect(saveResult.metadata.markdown).toEqual(null)
+ expect(saveResult.nullout).toEqual(['markdown'])
expect(saveResult.data).toEqual('xml only')
describe 'insertPrompt', ->
diff --git a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
index 5161e658e7..1df9587037 100644
--- a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
@@ -11,13 +11,13 @@ describe 'MarkdownEditingDescriptor', ->
@descriptor = new MarkdownEditingDescriptor($('.problem-editor'))
@descriptor.createXMLEditor('replace with markdown')
saveResult = @descriptor.save()
- expect(saveResult.metadata.markdown).toEqual(null)
+ expect(saveResult.nullout).toEqual(['markdown'])
expect(saveResult.data).toEqual('replace with markdown')
it 'saves xml from the xml editor', ->
loadFixtures 'problem-without-markdown.html'
@descriptor = new MarkdownEditingDescriptor($('.problem-editor'))
saveResult = @descriptor.save()
- expect(saveResult.metadata.markdown).toEqual(null)
+ expect(saveResult.nullout).toEqual(['markdown'])
expect(saveResult.data).toEqual('xml only')
describe 'insertMultipleChoice', ->
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index e29276936b..b7dbf6864d 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -22,7 +22,6 @@ class @Problem
@$('section.action input:button').click @refreshAnswers
@$('section.action input.check').click @check_fd
- #@$('section.action input.check').click @check
@$('section.action input.reset').click @reset
@$('section.action button.show').click @show
@$('section.action input.save').click @save
@@ -162,9 +161,6 @@ class @Problem
# maybe preferable to consolidate all dispatches to use FormData
###
check_fd: =>
- # Calling check from check_fd will result in firing the 'problem_check' event twice, since it is also called in the check function.
- #Logger.log 'problem_check', @answers
-
# If there are no file inputs in the problem, we can fall back on @check
if $('input:file').length == 0
@check()
@@ -239,6 +235,12 @@ class @Problem
check: =>
@check_waitfor()
Logger.log 'problem_check', @answers
+
+ # Segment.io
+ analytics.track "Problem Checked",
+ problem_id: @id
+ answers: @answers
+
$.postWithPrefix "#{@url}/problem_check", @answers, (response) =>
switch response.success
when 'incorrect', 'correct'
diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee
index 1b7f9bb4fb..4cdd571c65 100644
--- a/common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee
+++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee
@@ -153,8 +153,7 @@ Write a persuasive essay to a newspaper reflecting your vies on censorship in li
else
{
data: @xml_editor.getValue()
- metadata:
- markdown: null
+ nullout: ['markdown']
}
@insertRubric: (selectedText) ->
diff --git a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee
index b723f230e9..bd2871eb61 100644
--- a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee
+++ b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee
@@ -123,9 +123,8 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
}
else
{
- data: @xml_editor.getValue()
- metadata:
- markdown: null
+ data: @xml_editor.getValue()
+ nullout: ['markdown']
}
@insertMultipleChoice: (selectedText) ->
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index 2fa12e2e90..eb721dfc99 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -310,14 +310,7 @@ class ModuleStore(object):
"""
raise NotImplementedError
- def clone_item(self, source, location):
- """
- Clone a new item that is a copy of the item at the location `source`
- and writes it to `location`
- """
- raise NotImplementedError
-
- def update_item(self, location, data):
+ def update_item(self, location, data, allow_not_found=False):
"""
Set the data in the item specified by the location to
data
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/base.py b/common/lib/xmodule/xmodule/modulestore/mongo/base.py
index f56393d75e..ab63243aaf 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo/base.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo/base.py
@@ -33,7 +33,7 @@ from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError
from xblock.core import Scope
from xmodule.modulestore import ModuleStoreBase, Location, namedtuple_to_son
-from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateItemError
+from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata, INHERITABLE_METADATA, inherit_metadata
log = logging.getLogger(__name__)
@@ -62,11 +62,12 @@ class MongoKeyValueStore(KeyValueStore):
A KeyValueStore that maps keyed data access to one of the 3 data areas
known to the MongoModuleStore (data, children, and metadata)
"""
- def __init__(self, data, children, metadata, location):
+ def __init__(self, data, children, metadata, location, category):
self._data = data
self._children = children
self._metadata = metadata
self._location = location
+ self._category = category
def get(self, key):
if key.scope == Scope.children:
@@ -78,6 +79,8 @@ class MongoKeyValueStore(KeyValueStore):
elif key.scope == Scope.content:
if key.field_name == 'location':
return self._location
+ elif key.field_name == 'category':
+ return self._category
elif key.field_name == 'data' and not isinstance(self._data, dict):
return self._data
else:
@@ -93,6 +96,8 @@ class MongoKeyValueStore(KeyValueStore):
elif key.scope == Scope.content:
if key.field_name == 'location':
self._location = value
+ elif key.field_name == 'category':
+ self._category = value
elif key.field_name == 'data' and not isinstance(self._data, dict):
self._data = value
else:
@@ -109,6 +114,8 @@ class MongoKeyValueStore(KeyValueStore):
elif key.scope == Scope.content:
if key.field_name == 'location':
self._location = Location(None)
+ elif key.field_name == 'category':
+ self._category = None
elif key.field_name == 'data' and not isinstance(self._data, dict):
self._data = None
else:
@@ -123,7 +130,10 @@ class MongoKeyValueStore(KeyValueStore):
return key.field_name in self._metadata
elif key.scope == Scope.content:
if key.field_name == 'location':
+ # WHY TRUE? if it's been deleted should it be False?
return True
+ elif key.field_name == 'category':
+ return self._category is not None
elif key.field_name == 'data' and not isinstance(self._data, dict):
return True
else:
@@ -185,8 +195,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
else:
# load the module and apply the inherited metadata
try:
+ category = json_data['location']['category']
class_ = XModuleDescriptor.load_class(
- json_data['location']['category'],
+ category,
self.default_class
)
definition = json_data.get('definition', {})
@@ -201,9 +212,12 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
definition.get('children', []),
metadata,
location,
+ category
)
model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location))
+ model_data['category'] = category
+ model_data['location'] = location
module = class_(self, model_data)
if self.cached_metadata is not None:
# parent container pointers don't differentiate between draft and non-draft
@@ -217,6 +231,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
return ErrorDescriptor.from_json(
json_data,
self,
+ json_data['location'],
error_msg=exc_info_to_str(sys.exc_info())
)
@@ -582,51 +597,93 @@ class MongoModuleStore(ModuleStoreBase):
modules = self._load_items(list(items), depth)
return modules
- def clone_item(self, source, location):
+ def create_xmodule(self, location, definition_data=None, metadata=None, system=None):
"""
- Clone a new item that is a copy of the item at the location `source`
- and writes it to `location`
+ Create the new xmodule but don't save it. Returns the new module.
+
+ :param location: a Location--must have a category
+ :param definition_data: can be empty. The initial definition_data for the kvs
+ :param metadata: can be empty, the initial metadata for the kvs
+ :param system: if you already have an xmodule from the course, the xmodule.system value
"""
- item = None
- try:
- source_item = self.collection.find_one(location_to_query(source))
-
- # allow for some programmatically generated substitutions in metadata, e.g. Discussion_id's should be auto-generated
- for key in source_item['metadata'].keys():
- if source_item['metadata'][key] == '$$GUID$$':
- source_item['metadata'][key] = uuid4().hex
-
- source_item['_id'] = Location(location).dict()
- self.collection.insert(
- source_item,
- # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
- # from overriding our default value set in the init method.
- safe=self.collection.safe
+ if not isinstance(location, Location):
+ location = Location(location)
+ # differs from split mongo in that I believe most of this logic should be above the persistence
+ # layer but added it here to enable quick conversion. I'll need to reconcile these.
+ if metadata is None:
+ metadata = {}
+ if system is None:
+ system = CachingDescriptorSystem(
+ self,
+ {},
+ self.default_class,
+ None,
+ self.error_tracker,
+ self.render_template,
+ {}
)
- item = self._load_items([source_item])[0]
+ xblock_class = XModuleDescriptor.load_class(location.category, self.default_class)
+ if definition_data is None:
+ if hasattr(xblock_class, 'data') and getattr(xblock_class, 'data').default is not None:
+ definition_data = getattr(xblock_class, 'data').default
+ else:
+ definition_data = {}
+ dbmodel = self._create_new_model_data(location.category, location, definition_data, metadata)
+ xmodule = xblock_class(system, dbmodel)
+ return xmodule
- # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
- # if we add one then we need to also add it to the policy information (i.e. metadata)
- # we should remove this once we can break this reference from the course to static tabs
- if location.category == 'static_tab':
- course = self.get_course_for_item(item.location)
- existing_tabs = course.tabs or []
- existing_tabs.append({
- 'type': 'static_tab',
- 'name': item.display_name,
- 'url_slug': item.location.name
- })
- course.tabs = existing_tabs
- self.update_metadata(course.location, course._model_data._kvs._metadata)
-
- except pymongo.errors.DuplicateKeyError:
- raise DuplicateItemError(location)
+ def save_xmodule(self, xmodule):
+ """
+ Save the given xmodule (will either create or update based on whether id already exists).
+ Pulls out the data definition v metadata v children locally but saves it all.
+ :param xmodule:
+ """
+ # split mongo's persist_dag is more general and useful.
+ self.collection.save({
+ '_id': xmodule.location.dict(),
+ 'metadata': own_metadata(xmodule),
+ 'definition': {
+ 'data': xmodule.xblock_kvs._data,
+ 'children': xmodule.children if xmodule.has_children else []
+ }
+ })
# recompute (and update) the metadata inheritance tree which is cached
- self.refresh_cached_metadata_inheritance_tree(Location(location))
- self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
+ self.refresh_cached_metadata_inheritance_tree(xmodule.location)
+ self.fire_updated_modulestore_signal(get_course_id_no_run(xmodule.location), xmodule.location)
- return item
+ def create_and_save_xmodule(self, location, definition_data=None, metadata=None, system=None):
+ """
+ Create the new xmodule and save it. Does not return the new module because if the caller
+ will insert it as a child, it's inherited metadata will completely change. The difference
+ between this and just doing create_xmodule and save_xmodule is this ensures static_tabs get
+ pointed to by the course.
+
+ :param location: a Location--must have a category
+ :param definition_data: can be empty. The initial definition_data for the kvs
+ :param metadata: can be empty, the initial metadata for the kvs
+ :param system: if you already have an xmodule from the course, the xmodule.system value
+ """
+ # differs from split mongo in that I believe most of this logic should be above the persistence
+ # layer but added it here to enable quick conversion. I'll need to reconcile these.
+ new_object = self.create_xmodule(location, definition_data, metadata, system)
+ location = new_object.location
+ self.save_xmodule(new_object)
+
+ # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
+ # if we add one then we need to also add it to the policy information (i.e. metadata)
+ # we should remove this once we can break this reference from the course to static tabs
+ # TODO move this special casing to app tier (similar to attaching new element to parent)
+ if location.category == 'static_tab':
+ course = self.get_course_for_item(location)
+ existing_tabs = course.tabs or []
+ existing_tabs.append({
+ 'type': 'static_tab',
+ 'name': new_object.display_name,
+ 'url_slug': new_object.location.name
+ })
+ course.tabs = existing_tabs
+ self.update_metadata(course.location, course.xblock_kvs._metadata)
def fire_updated_modulestore_signal(self, course_id, location):
"""
@@ -683,7 +740,7 @@ class MongoModuleStore(ModuleStoreBase):
if result['n'] == 0:
raise ItemNotFoundError(location)
- def update_item(self, location, data):
+ def update_item(self, location, data, allow_not_found=False):
"""
Set the data in the item specified by the location to
data
@@ -691,8 +748,11 @@ class MongoModuleStore(ModuleStoreBase):
location: Something that can be passed to Location
data: A nested dictionary of problem data
"""
-
- self._update_single_item(location, {'definition.data': data})
+ try:
+ self._update_single_item(location, {'definition.data': data})
+ except ItemNotFoundError:
+ if not allow_not_found:
+ raise
def update_children(self, location, children):
"""
@@ -775,3 +835,24 @@ class MongoModuleStore(ModuleStoreBase):
are loaded on demand, rather than up front
"""
return {}
+
+ def _create_new_model_data(self, category, location, definition_data, metadata):
+ """
+ To instantiate a new xmodule which will be saved latter, set up the dbModel and kvs
+ """
+ kvs = MongoKeyValueStore(
+ definition_data,
+ [],
+ metadata,
+ location,
+ category
+ )
+
+ class_ = XModuleDescriptor.load_class(
+ category,
+ self.default_class
+ )
+ model_data = DbModel(kvs, class_, None, MongoUsage(None, location))
+ model_data['category'] = category
+ model_data['location'] = location
+ return model_data
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py
index f34c3a53f9..d289e03739 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py
@@ -8,11 +8,12 @@ and otherwise returns i4x://org/course/cat/name).
from datetime import datetime
-from xmodule.modulestore import Location, namedtuple_to_son
-from xmodule.modulestore.exceptions import ItemNotFoundError
-from xmodule.modulestore.inheritance import own_metadata
from xmodule.exceptions import InvalidVersionError
-from xmodule.modulestore.mongo.base import MongoModuleStore
+from xmodule.modulestore import Location, namedtuple_to_son
+from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateItemError
+from xmodule.modulestore.inheritance import own_metadata
+from xmodule.modulestore.mongo.base import location_to_query, get_course_id_no_run, MongoModuleStore
+import pymongo
from pytz import UTC
DRAFT = 'draft'
@@ -92,6 +93,21 @@ class DraftModuleStore(MongoModuleStore):
except ItemNotFoundError:
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=depth))
+ def create_xmodule(self, location, definition_data=None, metadata=None, system=None):
+ """
+ Create the new xmodule but don't save it. Returns the new module with a draft locator
+
+ :param location: a Location--must have a category
+ :param definition_data: can be empty. The initial definition_data for the kvs
+ :param metadata: can be empty, the initial metadata for the kvs
+ :param system: if you already have an xmodule from the course, the xmodule.system value
+ """
+ draft_loc = as_draft(location)
+ if draft_loc.category in DIRECT_ONLY_CATEGORIES:
+ raise InvalidVersionError(location)
+ return super(DraftModuleStore, self).create_xmodule(draft_loc, definition_data, metadata, system)
+
+
def get_items(self, location, course_id=None, depth=0):
"""
Returns a list of XModuleDescriptor instances for the items
@@ -119,14 +135,26 @@ class DraftModuleStore(MongoModuleStore):
]
return [wrap_draft(item) for item in draft_items + non_draft_items]
- def clone_item(self, source, location):
+ def convert_to_draft(self, source_location):
"""
- Clone a new item that is a copy of the item at the location `source`
- and writes it to `location`
+ Create a copy of the source and mark its revision as draft.
+
+ :param source: the location of the source (its revision must be None)
"""
- if Location(location).category in DIRECT_ONLY_CATEGORIES:
- raise InvalidVersionError(location)
- return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location)))
+ original = self.collection.find_one(location_to_query(source_location))
+ draft_location = as_draft(source_location)
+ if draft_location.category in DIRECT_ONLY_CATEGORIES:
+ raise InvalidVersionError(source_location)
+ original['_id'] = draft_location.dict()
+ try:
+ self.collection.insert(original)
+ except pymongo.errors.DuplicateKeyError:
+ raise DuplicateItemError(original['_id'])
+
+ self.refresh_cached_metadata_inheritance_tree(draft_location)
+ self.fire_updated_modulestore_signal(get_course_id_no_run(draft_location), draft_location)
+
+ return self._load_items([original])[0]
def update_item(self, location, data, allow_not_found=False):
"""
@@ -140,7 +168,7 @@ class DraftModuleStore(MongoModuleStore):
try:
draft_item = self.get_item(location)
if not getattr(draft_item, 'is_draft', False):
- self.clone_item(location, draft_loc)
+ self.convert_to_draft(location)
except ItemNotFoundError, e:
if not allow_not_found:
raise e
@@ -158,7 +186,7 @@ class DraftModuleStore(MongoModuleStore):
draft_loc = as_draft(location)
draft_item = self.get_item(location)
if not getattr(draft_item, 'is_draft', False):
- self.clone_item(location, draft_loc)
+ self.convert_to_draft(location)
return super(DraftModuleStore, self).update_children(draft_loc, children)
@@ -174,7 +202,7 @@ class DraftModuleStore(MongoModuleStore):
draft_item = self.get_item(location)
if not getattr(draft_item, 'is_draft', False):
- self.clone_item(location, draft_loc)
+ self.convert_to_draft(location)
if 'is_draft' in metadata:
del metadata['is_draft']
@@ -218,9 +246,7 @@ class DraftModuleStore(MongoModuleStore):
"""
Turn the published version into a draft, removing the published version
"""
- if Location(location).category in DIRECT_ONLY_CATEGORIES:
- raise InvalidVersionError(location)
- super(DraftModuleStore, self).clone_item(location, as_draft(location))
+ self.convert_to_draft(location)
super(DraftModuleStore, self).delete_item(location)
def _query_children_for_cache_children(self, items):
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
index 6c5c1f66ca..564aac141d 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
@@ -5,7 +5,6 @@ from django.test import TestCase
from django.conf import settings
import xmodule.modulestore.django
-from xmodule.templates import update_templates
from unittest.util import safe_repr
@@ -48,7 +47,7 @@ def draft_mongo_store_config(data_dir):
return {
'default': {
- 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
+ 'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
'OPTIONS': modulestore_options
},
'direct': {
@@ -110,22 +109,6 @@ class ModuleStoreTestCase(TestCase):
modulestore.collection.remove(query)
modulestore.collection.drop()
- @staticmethod
- def load_templates_if_necessary():
- """
- Load templates into the direct modulestore only if they do not already exist.
- We need the templates, because they are copied to create
- XModules such as sections and problems.
- """
- modulestore = xmodule.modulestore.django.modulestore('direct')
-
- # Count the number of templates
- query = {"_id.course": "templates"}
- num_templates = modulestore.collection.find(query).count()
-
- if num_templates < 1:
- update_templates(modulestore)
-
@classmethod
def setUpClass(cls):
"""
@@ -169,9 +152,6 @@ class ModuleStoreTestCase(TestCase):
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
- # Check that we have templates loaded; if not, load them
- ModuleStoreTestCase.load_templates_if_necessary()
-
# Call superclass implementation
super(ModuleStoreTestCase, self)._pre_setup()
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
index 457a88482a..be705149f3 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
@@ -1,15 +1,14 @@
-from factory import Factory, lazy_attribute_sequence, lazy_attribute
-from uuid import uuid4
import datetime
+from factory import Factory, LazyAttributeSequence
+from uuid import uuid4
+from pytz import UTC
+
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
-from xmodule.modulestore.inheritance import own_metadata
-from xmodule.x_module import ModuleSystem
-from mitxmako.shortcuts import render_to_string
-from xblock.runtime import InvalidScopeError
-from pytz import UTC
-
+from xmodule.course_module import CourseDescriptor
+from xblock.core import Scope
+from xmodule.x_module import XModuleDescriptor
class XModuleCourseFactory(Factory):
"""
@@ -21,9 +20,8 @@ class XModuleCourseFactory(Factory):
@classmethod
def _create(cls, target_class, **kwargs):
- template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
org = kwargs.pop('org', None)
- number = kwargs.pop('number', None)
+ number = kwargs.pop('number', kwargs.pop('course', None))
display_name = kwargs.pop('display_name', None)
location = Location('i4x', org, number, 'course', Location.clean(display_name))
@@ -33,13 +31,13 @@ class XModuleCourseFactory(Factory):
store = modulestore()
# Write the data to the mongo datastore
- new_course = store.clone_item(template, location)
+ new_course = store.create_xmodule(location)
# This metadata code was copied from cms/djangoapps/contentstore/views.py
if display_name is not None:
new_course.display_name = display_name
- new_course.lms.start = datetime.datetime.now(UTC)
+ new_course.lms.start = datetime.datetime.now(UTC).replace(microsecond=0)
new_course.tabs = kwargs.pop(
'tabs',
[
@@ -56,13 +54,7 @@ class XModuleCourseFactory(Factory):
setattr(new_course, k, v)
# Update the data in the mongo datastore
- store.update_metadata(new_course.location, own_metadata(new_course))
- store.update_item(new_course.location, new_course._model_data._kvs._data)
-
- # update_item updates the the course as it exists in the modulestore, but doesn't
- # update the instance we are working with, so have to refetch the course after updating it.
- new_course = store.get_instance(new_course.id, new_course.location)
-
+ store.save_xmodule(new_course)
return new_course
@@ -73,7 +65,6 @@ class Course:
class CourseFactory(XModuleCourseFactory):
FACTORY_FOR = Course
- template = 'i4x://edx/templates/course/Empty'
org = 'MITx'
number = '999'
display_name = 'Robot Super Course'
@@ -86,76 +77,72 @@ class XModuleItemFactory(Factory):
ABSTRACT_FACTORY = True
- display_name = None
+ parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
+ category = 'problem'
+ display_name = LazyAttributeSequence(lambda o, n: "{} {}".format(o.category, n))
- @lazy_attribute
- def category(attr):
- template = Location(attr.template)
- return template.category
-
- @lazy_attribute
- def location(attr):
- parent = Location(attr.parent_location)
- dest_name = attr.display_name.replace(" ", "_") if attr.display_name is not None else uuid4().hex
- return parent._replace(category=attr.category, name=dest_name)
+ @staticmethod
+ def location(parent, category, display_name):
+ dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex
+ return Location(parent).replace(category=category, name=dest_name)
@classmethod
def _create(cls, target_class, **kwargs):
"""
- Uses *kwargs*:
+ Uses ``**kwargs``:
- *parent_location* (required): the location of the parent module
+ :parent_location: (required): the location of the parent module
(e.g. the parent course or section)
- *template* (required): the template to create the item from
- (e.g. i4x://templates/section/Empty)
+ :category: the category of the resulting item.
- *data* (optional): the data for the item
+ :data: (optional): the data for the item
(e.g. XML problem definition for a problem item)
- *display_name* (optional): the display name of the item
+ :display_name: (optional): the display name of the item
- *metadata* (optional): dictionary of metadata attributes
+ :metadata: (optional): dictionary of metadata attributes
- *target_class* is ignored
+ :boilerplate: (optional) the boilerplate for overriding field values
+
+ :target_class: is ignored
"""
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
-
+ # catch any old style users before they get into trouble
+ assert not 'template' in kwargs
parent_location = Location(kwargs.get('parent_location'))
- template = Location(kwargs.get('template'))
data = kwargs.get('data')
+ category = kwargs.get('category')
display_name = kwargs.get('display_name')
metadata = kwargs.get('metadata', {})
+ location = kwargs.get('location', XModuleItemFactory.location(parent_location, category, display_name))
+ assert location != parent_location
+ if kwargs.get('boilerplate') is not None:
+ template_id = kwargs.get('boilerplate')
+ clz = XModuleDescriptor.load_class(category)
+ template = clz.get_template(template_id)
+ assert template is not None
+ metadata.update(template.get('metadata', {}))
+ if not isinstance(data, basestring):
+ data.update(template.get('data'))
store = modulestore('direct')
# This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location)
- new_item = store.clone_item(template, kwargs.get('location'))
-
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
- new_item.display_name = display_name
+ metadata['display_name'] = display_name
+ # note that location comes from above lazy_attribute
+ store.create_and_save_xmodule(location, metadata=metadata, definition_data=data)
- # Add additional metadata or override current metadata
- item_metadata = own_metadata(new_item)
- item_metadata.update(metadata)
- store.update_metadata(new_item.location.url(), item_metadata)
+ if location.category not in DETACHED_CATEGORIES:
+ parent.children.append(location.url())
+ store.update_children(parent_location, parent.children)
- # replace the data with the optional *data* parameter
- if data is not None:
- store.update_item(new_item.location, data)
-
- if new_item.location.category not in DETACHED_CATEGORIES:
- store.update_children(parent_location, parent.children + [new_item.location.url()])
-
- # update_children updates the the item as it exists in the modulestore, but doesn't
- # update the instance we are working with, so have to refetch the item after updating it.
- new_item = store.get_item(new_item.location)
-
- return new_item
+ return store.get_item(location)
class Item:
@@ -164,40 +151,4 @@ class Item:
class ItemFactory(XModuleItemFactory):
FACTORY_FOR = Item
-
- parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
- template = 'i4x://edx/templates/chapter/Empty'
-
- @lazy_attribute_sequence
- def display_name(attr, n):
- return "{} {}".format(attr.category.title(), n)
-
-
-def get_test_xmodule_for_descriptor(descriptor):
- """
- Attempts to create an xmodule which responds usually correctly from the descriptor. Not guaranteed.
-
- :param descriptor:
- """
- module_sys = ModuleSystem(
- ajax_url='',
- track_function=None,
- get_module=None,
- render_template=render_to_string,
- replace_urls=None,
- xblock_model_data=_test_xblock_model_data_accessor(descriptor)
- )
- return descriptor.xmodule(module_sys)
-
-
-def _test_xblock_model_data_accessor(descriptor):
- simple_map = {}
- for field in descriptor.fields:
- try:
- simple_map[field.name] = getattr(descriptor, field.name)
- except InvalidScopeError:
- simple_map[field.name] = field.default
- for field in descriptor.module_class.fields:
- if field.name not in simple_map:
- simple_map[field.name] = field.default
- return lambda o: simple_map
+ category = 'chapter'
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
index 44e69fb0ed..c149724cc7 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
@@ -9,7 +9,6 @@ from xblock.runtime import KeyValueStore, InvalidScopeError
from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore
from xmodule.modulestore.xml_importer import import_from_xml
-from xmodule.templates import update_templates
from .test_modulestore import check_path_to_location
from . import DATA_DIR
@@ -51,7 +50,6 @@ class TestMongoModuleStore(object):
# Explicitly list the courses to load (don't want the big one)
courses = ['toy', 'simple']
import_from_xml(store, DATA_DIR, courses)
- update_templates(store)
return store
@staticmethod
@@ -126,7 +124,7 @@ class TestMongoKeyValueStore(object):
self.location = Location('i4x://org/course/category/name@version')
self.children = ['i4x://org/course/child/a', 'i4x://org/course/child/b']
self.metadata = {'meta': 'meta_val'}
- self.kvs = MongoKeyValueStore(self.data, self.children, self.metadata, self.location)
+ self.kvs = MongoKeyValueStore(self.data, self.children, self.metadata, self.location, 'category')
def _check_read(self, key, expected_value):
assert_equals(expected_value, self.kvs.get(key))
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 26c8b9bfca..012740ff9a 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -463,7 +463,10 @@ class XMLModuleStore(ModuleStoreBase):
# tabs are referenced in policy.json through a 'slug' which is just the filename without the .html suffix
slug = os.path.splitext(os.path.basename(filepath))[0]
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
- module = HtmlDescriptor(system, {'data': html, 'location': loc})
+ module = HtmlDescriptor(
+ system,
+ {'data': html, 'location': loc, 'category': category}
+ )
# VS[compat]:
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
# from the course policy
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
index 1fe62035e6..60eb9dc749 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
@@ -810,7 +810,6 @@ class CombinedOpenEndedV1Descriptor():
filename_extension = "xml"
has_score = True
- template_dir_name = "combinedopenended"
def __init__(self, system):
self.system = system
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 0f0851fbf7..8d8a85f788 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -730,7 +730,6 @@ class OpenEndedDescriptor():
filename_extension = "xml"
has_score = True
- template_dir_name = "openended"
def __init__(self, system):
self.system = system
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
index a5498289e2..1262e1f68f 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
@@ -287,7 +287,6 @@ class SelfAssessmentDescriptor():
filename_extension = "xml"
has_score = True
- template_dir_name = "selfassessment"
def __init__(self, system):
self.system = system
diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py
index 7df444a892..03003ed1e5 100644
--- a/common/lib/xmodule/xmodule/peer_grading_module.py
+++ b/common/lib/xmodule/xmodule/peer_grading_module.py
@@ -59,6 +59,15 @@ class PeerGradingFields(object):
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
scope=Scope.settings, values={"min": 0, "step": ".1"}
)
+ display_name = String(
+ display_name="Display Name",
+ help="Display name for this module",
+ scope=Scope.settings,
+ default="Peer Grading Interface"
+ )
+ data = String(help="Html contents to display for this module",
+ default='
About This Course
- About This Course
+ Prerequisites
- Prerequisites
+ Course Staff
-
- Course Staff
+
+ Staff Member #1
- Staff Member #1
+
-
+ Staff Member #2
- Staff Member #2
+ Frequently Asked Questions
- Do I need to buy a textbook?
- Frequently Asked Questions
+ Do I need to buy a textbook?
+ Question #2
- Question #2
+ <annotation> tag which may may have the following attributes:
-
- title (optional). Title of the annotation. Defaults to Commentary if omitted.body (required). Text of the annotation.problem (optional). Numeric index of the problem associated with this annotation. This is a zero-based index, so the first problem on the page would have problem="0".highlight (optional). Possible values: yellow, red, orange, green, blue, or purple. Defaults to yellow if this attribute is omitted."
-children: []
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/templates/default/empty.yaml b/common/lib/xmodule/xmodule/templates/default/empty.yaml
deleted file mode 100644
index a2fb2b5832..0000000000
--- a/common/lib/xmodule/xmodule/templates/default/empty.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-metadata:
- display_name: Empty
-data: ""
-children: []
diff --git a/common/lib/xmodule/xmodule/templates/discussion/default.yaml b/common/lib/xmodule/xmodule/templates/discussion/default.yaml
deleted file mode 100644
index 049e34b3e7..0000000000
--- a/common/lib/xmodule/xmodule/templates/discussion/default.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-metadata:
- display_name: Discussion Tag
- for: Topic-Level Student-Visible Label
- id: $$GUID$$
- discussion_category: Week 1
-data: |
-
-children: []
diff --git a/common/lib/xmodule/xmodule/templates/html/empty.yaml b/common/lib/xmodule/xmodule/templates/html/empty.yaml
deleted file mode 100644
index 40b005af28..0000000000
--- a/common/lib/xmodule/xmodule/templates/html/empty.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
----
-metadata:
- display_name: Blank HTML Page
-
-data: |
-
-children: []
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/templates/html/everything.yaml b/common/lib/xmodule/xmodule/templates/html/everything.yaml
deleted file mode 100644
index 348ce64fa1..0000000000
--- a/common/lib/xmodule/xmodule/templates/html/everything.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
----
-metadata:
- display_name: Announcement
-
-data: |
- Heading of document
- First subheading
- Links
-
-
-
-
- 



-
+
-
-
+
+
A text input problem accepts a line of text from the @@ -46,4 +40,3 @@ data: |
This is where you can add additional pages to your courseware. Click the 'edit' button to begin editing.
" -children: [] \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/video/default.yaml b/common/lib/xmodule/xmodule/templates/video/default.yaml deleted file mode 100644 index 048e7396c7..0000000000 --- a/common/lib/xmodule/xmodule/templates/video/default.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -metadata: - display_name: default -data: "" -children: [] diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml deleted file mode 100644 index 1c25b272a3..0000000000 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -metadata: - display_name: Video Alpha - version: 1 -data: | -