Commit 08483604 by Don Mitchell

Merge pull request #844 from edx/dhm/editable_metadata

refactoring of platform to xblock 0.3 w/ refactoring of inheritance in the platform to a consistent representation.
parents d3580994 0cac1c3b
...@@ -64,11 +64,11 @@ def set_module_info(store, location, post_data): ...@@ -64,11 +64,11 @@ def set_module_info(store, location, post_data):
if posted_metadata[metadata_key] is None: if posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module._model_data: if module._field_data.has(module, metadata_key):
del module._model_data[metadata_key] module._field_data.delete(module, metadata_key)
del posted_metadata[metadata_key] del posted_metadata[metadata_key]
else: else:
module._model_data[metadata_key] = value module._field_data.set(module, metadata_key, value)
# commit to datastore # commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
......
...@@ -219,7 +219,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -219,7 +219,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'course', '2012_Fall', None]), depth=None) 'course', '2012_Fall', None]), depth=None)
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod) self.assertEqual(html_module.graceperiod, course.graceperiod)
self.assertNotIn('graceperiod', own_metadata(html_module)) self.assertNotIn('graceperiod', own_metadata(html_module))
draft_store.convert_to_draft(html_module.location) draft_store.convert_to_draft(html_module.location)
...@@ -227,7 +227,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -227,7 +227,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# refetch to check metadata # refetch to check metadata
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod) self.assertEqual(html_module.graceperiod, course.graceperiod)
self.assertNotIn('graceperiod', own_metadata(html_module)) self.assertNotIn('graceperiod', own_metadata(html_module))
# publish module # publish module
...@@ -236,7 +236,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -236,7 +236,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# refetch to check metadata # refetch to check metadata
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertEqual(html_module.lms.graceperiod, course.lms.graceperiod) self.assertEqual(html_module.graceperiod, course.graceperiod)
self.assertNotIn('graceperiod', own_metadata(html_module)) self.assertNotIn('graceperiod', own_metadata(html_module))
# put back in draft and change metadata and see if it's now marked as 'own_metadata' # put back in draft and change metadata and see if it's now marked as 'own_metadata'
...@@ -246,12 +246,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -246,12 +246,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
new_graceperiod = timedelta(hours=1) new_graceperiod = timedelta(hours=1)
self.assertNotIn('graceperiod', own_metadata(html_module)) self.assertNotIn('graceperiod', own_metadata(html_module))
html_module.lms.graceperiod = new_graceperiod html_module.graceperiod = new_graceperiod
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
html_module.save() html_module.save()
self.assertIn('graceperiod', own_metadata(html_module)) self.assertIn('graceperiod', own_metadata(html_module))
self.assertEqual(html_module.lms.graceperiod, new_graceperiod) self.assertEqual(html_module.graceperiod, new_graceperiod)
draft_store.update_metadata(html_module.location, own_metadata(html_module)) draft_store.update_metadata(html_module.location, own_metadata(html_module))
...@@ -259,7 +259,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -259,7 +259,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertIn('graceperiod', own_metadata(html_module)) self.assertIn('graceperiod', own_metadata(html_module))
self.assertEqual(html_module.lms.graceperiod, new_graceperiod) self.assertEqual(html_module.graceperiod, new_graceperiod)
# republish # republish
draft_store.publish(html_module.location, 0) draft_store.publish(html_module.location, 0)
...@@ -269,7 +269,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -269,7 +269,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
self.assertIn('graceperiod', own_metadata(html_module)) self.assertIn('graceperiod', own_metadata(html_module))
self.assertEqual(html_module.lms.graceperiod, new_graceperiod) self.assertEqual(html_module.graceperiod, new_graceperiod)
def test_get_depth_with_drafts(self): def test_get_depth_with_drafts(self):
import_from_xml(modulestore('direct'), 'common/test/data/', ['simple']) import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
...@@ -696,7 +696,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -696,7 +696,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# we want to assert equality between the objects, but we know the locations # we want to assert equality between the objects, but we know the locations
# differ, so just make them equal for testing purposes # differ, so just make them equal for testing purposes
source_item.location = new_loc source_item.scope_ids = source_item.scope_ids._replace(def_id=new_loc, usage_id=new_loc)
if hasattr(source_item, 'data') and hasattr(lookup_item, 'data'): if hasattr(source_item, 'data') and hasattr(lookup_item, 'data'):
self.assertEqual(source_item.data, lookup_item.data) self.assertEqual(source_item.data, lookup_item.data)
...@@ -877,7 +877,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -877,7 +877,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
depth=1 depth=1
) )
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case. # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
vertical.location = mongo.draft.as_draft(vertical.location.replace(name='no_references')) draft_loc = mongo.draft.as_draft(vertical.location.replace(name='no_references'))
vertical.scope_ids = vertical.scope_ids._replace(def_id=draft_loc, usage_id=draft_loc)
draft_store.save_xmodule(vertical) draft_store.save_xmodule(vertical)
orphan_vertical = draft_store.get_item(vertical.location) orphan_vertical = draft_store.get_item(vertical.location)
self.assertEqual(orphan_vertical.location.name, 'no_references') self.assertEqual(orphan_vertical.location.name, 'no_references')
...@@ -894,7 +896,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -894,7 +896,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
root_dir = path(mkdtemp_clean()) root_dir = path(mkdtemp_clean())
# now create a new/different private (draft only) vertical # 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_loc = mongo.draft.as_draft(Location(['i4x', 'edX', 'toy', 'vertical', 'a_private_vertical', None]))
vertical.scope_ids = vertical.scope_ids._replace(def_id=draft_loc, usage_id=draft_loc)
draft_store.save_xmodule(vertical) draft_store.save_xmodule(vertical)
private_vertical = draft_store.get_item(vertical.location) private_vertical = draft_store.get_item(vertical.location)
vertical = None # blank out b/c i destructively manipulated its location 2 lines above vertical = None # blank out b/c i destructively manipulated its location 2 lines above
...@@ -965,7 +968,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -965,7 +968,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(getattr(vertical, 'is_draft', False)) self.assertTrue(getattr(vertical, 'is_draft', False))
self.assertNotIn('index_in_children_list', child.xml_attributes) self.assertNotIn('index_in_children_list', child.xml_attributes)
self.assertNotIn('parent_sequential_url', vertical.xml_attributes) self.assertNotIn('parent_sequential_url', vertical.xml_attributes)
for child in vertical.get_children(): for child in vertical.get_children():
self.assertTrue(getattr(child, 'is_draft', False)) self.assertTrue(getattr(child, 'is_draft', False))
self.assertNotIn('index_in_children_list', child.xml_attributes) self.assertNotIn('index_in_children_list', child.xml_attributes)
...@@ -1628,8 +1631,8 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1628,8 +1631,8 @@ class ContentStoreTest(ModuleStoreTestCase):
# let's assert on the metadata_inheritance on an existing vertical # let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals: for vertical in verticals:
self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key) self.assertEqual(course.xqa_key, vertical.xqa_key)
self.assertEqual(course.start, vertical.lms.start) self.assertEqual(course.start, vertical.start)
self.assertGreater(len(verticals), 0) self.assertGreater(len(verticals), 0)
...@@ -1645,16 +1648,16 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1645,16 +1648,16 @@ class ContentStoreTest(ModuleStoreTestCase):
new_module = module_store.get_item(new_component_location) new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level # 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.graceperiod, new_module.graceperiod)
self.assertEqual(parent.lms.start, new_module.lms.start) self.assertEqual(parent.start, new_module.start)
self.assertEqual(course.start, new_module.lms.start) self.assertEqual(course.start, new_module.start)
self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key) self.assertEqual(course.xqa_key, new_module.xqa_key)
# #
# now let's define an override at the leaf node level # now let's define an override at the leaf node level
# #
new_module.lms.graceperiod = timedelta(1) new_module.graceperiod = timedelta(1)
new_module.save() new_module.save()
module_store.update_metadata(new_module.location, own_metadata(new_module)) module_store.update_metadata(new_module.location, own_metadata(new_module))
...@@ -1662,7 +1665,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1662,7 +1665,7 @@ class ContentStoreTest(ModuleStoreTestCase):
module_store.refresh_cached_metadata_inheritance_tree(new_component_location) module_store.refresh_cached_metadata_inheritance_tree(new_component_location)
new_module = module_store.get_item(new_component_location) new_module = module_store.get_item(new_component_location)
self.assertEqual(timedelta(1), new_module.lms.graceperiod) self.assertEqual(timedelta(1), new_module.graceperiod)
def test_default_metadata_inheritance(self): def test_default_metadata_inheritance(self):
course = CourseFactory.create() course = CourseFactory.create()
...@@ -1670,7 +1673,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1670,7 +1673,7 @@ class ContentStoreTest(ModuleStoreTestCase):
course.children.append(vertical) course.children.append(vertical)
# in memory # in memory
self.assertIsNotNone(course.start) self.assertIsNotNone(course.start)
self.assertEqual(course.start, vertical.lms.start) self.assertEqual(course.start, vertical.start)
self.assertEqual(course.textbooks, []) self.assertEqual(course.textbooks, [])
self.assertIn('GRADER', course.grading_policy) self.assertIn('GRADER', course.grading_policy)
self.assertIn('GRADE_CUTOFFS', course.grading_policy) self.assertIn('GRADE_CUTOFFS', course.grading_policy)
...@@ -1682,7 +1685,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1682,7 +1685,7 @@ class ContentStoreTest(ModuleStoreTestCase):
fetched_item = module_store.get_item(vertical.location) fetched_item = module_store.get_item(vertical.location)
self.assertIsNotNone(fetched_course.start) self.assertIsNotNone(fetched_course.start)
self.assertEqual(course.start, fetched_course.start) self.assertEqual(course.start, fetched_course.start)
self.assertEqual(fetched_course.start, fetched_item.lms.start) self.assertEqual(fetched_course.start, fetched_item.start)
self.assertEqual(course.textbooks, fetched_course.textbooks) self.assertEqual(course.textbooks, fetched_course.textbooks)
# is this test too strict? i.e., it requires the dicts to be == # is this test too strict? i.e., it requires the dicts to be ==
self.assertEqual(course.checklists, fetched_course.checklists) self.assertEqual(course.checklists, fetched_course.checklists)
...@@ -1755,12 +1758,10 @@ class MetadataSaveTestCase(ModuleStoreTestCase): ...@@ -1755,12 +1758,10 @@ class MetadataSaveTestCase(ModuleStoreTestCase):
'track' 'track'
} }
fields = self.video_descriptor.fields
location = self.video_descriptor.location location = self.video_descriptor.location
for field in fields: for field_name in attrs_to_strip:
if field.name in attrs_to_strip: delattr(self.video_descriptor, field_name)
field.delete_from(self.video_descriptor)
self.assertNotIn('html5_sources', own_metadata(self.video_descriptor)) self.assertNotIn('html5_sources', own_metadata(self.video_descriptor))
get_modulestore(location).update_metadata( get_modulestore(location).update_metadata(
......
...@@ -343,8 +343,8 @@ class CourseGradingTest(CourseTestCase): ...@@ -343,8 +343,8 @@ class CourseGradingTest(CourseTestCase):
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
self.assertEqual('Not Graded', section_grader_type['graderType']) self.assertEqual('Not Graded', section_grader_type['graderType'])
self.assertEqual(None, descriptor.lms.format) self.assertEqual(None, descriptor.format)
self.assertEqual(False, descriptor.lms.graded) self.assertEqual(False, descriptor.graded)
# Change the default grader type to Homework, which should also mark the section as graded # Change the default grader type to Homework, which should also mark the section as graded
CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Homework'}) CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Homework'})
...@@ -352,8 +352,8 @@ class CourseGradingTest(CourseTestCase): ...@@ -352,8 +352,8 @@ class CourseGradingTest(CourseTestCase):
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
self.assertEqual('Homework', section_grader_type['graderType']) self.assertEqual('Homework', section_grader_type['graderType'])
self.assertEqual('Homework', descriptor.lms.format) self.assertEqual('Homework', descriptor.format)
self.assertEqual(True, descriptor.lms.graded) self.assertEqual(True, descriptor.graded)
# Change the grader type back to Not Graded, which should also unmark the section as graded # Change the grader type back to Not Graded, which should also unmark the section as graded
CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Not Graded'}) CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Not Graded'})
...@@ -361,8 +361,8 @@ class CourseGradingTest(CourseTestCase): ...@@ -361,8 +361,8 @@ class CourseGradingTest(CourseTestCase):
section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location)
self.assertEqual('Not Graded', section_grader_type['graderType']) self.assertEqual('Not Graded', section_grader_type['graderType'])
self.assertEqual(None, descriptor.lms.format) self.assertEqual(None, descriptor.format)
self.assertEqual(False, descriptor.lms.graded) self.assertEqual(False, descriptor.graded)
class CourseMetadataEditingTest(CourseTestCase): class CourseMetadataEditingTest(CourseTestCase):
......
...@@ -218,20 +218,16 @@ class TemplateTests(unittest.TestCase): ...@@ -218,20 +218,16 @@ class TemplateTests(unittest.TestCase):
) )
usage_id = json_data.get('_id', None) usage_id = json_data.get('_id', None)
if not '_inherited_settings' in json_data and parent_xblock is not None: if not '_inherited_settings' in json_data and parent_xblock is not None:
json_data['_inherited_settings'] = parent_xblock.xblock_kvs.get_inherited_settings().copy() json_data['_inherited_settings'] = parent_xblock.xblock_kvs.inherited_settings.copy()
json_fields = json_data.get('fields', {}) json_fields = json_data.get('fields', {})
for field in inheritance.INHERITABLE_METADATA: for field_name in inheritance.InheritanceMixin.fields:
if field in json_fields: if field_name in json_fields:
json_data['_inherited_settings'][field] = json_fields[field] json_data['_inherited_settings'][field_name] = json_fields[field_name]
new_block = system.xblock_from_json(class_, usage_id, json_data) new_block = system.xblock_from_json(class_, usage_id, json_data)
if parent_xblock is not None: if parent_xblock is not None:
children = parent_xblock.children parent_xblock.children.append(new_block.scope_ids.usage_id)
children.append(new_block) # decache pending children field settings
# trigger setter method by using top level field access
parent_xblock.children = children
# decache pending children field settings (Note, truly persisting at this point would break b/c
# persistence assumes children is a list of ids not actual xblocks)
parent_xblock.save() parent_xblock.save()
return new_block return new_block
...@@ -97,9 +97,9 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): ...@@ -97,9 +97,9 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
self.assertIsNotNone(content) self.assertIsNotNone(content)
# make sure course.lms.static_asset_path is correct # make sure course.static_asset_path is correct
print "static_asset_path = {0}".format(course.lms.static_asset_path) print "static_asset_path = {0}".format(course.static_asset_path)
self.assertEqual(course.lms.static_asset_path, 'test_import_course') self.assertEqual(course.static_asset_path, 'test_import_course')
def test_asset_import_nostatic(self): def test_asset_import_nostatic(self):
''' '''
......
from contentstore.tests.test_course_settings import CourseTestCase from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
...@@ -69,7 +69,7 @@ class TestCreateItem(CourseTestCase): ...@@ -69,7 +69,7 @@ class TestCreateItem(CourseTestCase):
# get the new item and check its category and display_name # get the new item and check its category and display_name
chap_location = self.response_id(resp) chap_location = self.response_id(resp)
new_obj = modulestore().get_item(chap_location) new_obj = modulestore().get_item(chap_location)
self.assertEqual(new_obj.category, 'chapter') self.assertEqual(new_obj.scope_ids.block_type, 'chapter')
self.assertEqual(new_obj.display_name, display_name) self.assertEqual(new_obj.display_name, display_name)
self.assertEqual(new_obj.location.org, self.course.location.org) self.assertEqual(new_obj.location.org, self.course.location.org)
self.assertEqual(new_obj.location.course, self.course.location.course) self.assertEqual(new_obj.location.course, self.course.location.course)
...@@ -226,7 +226,7 @@ class TestEditItem(CourseTestCase): ...@@ -226,7 +226,7 @@ class TestEditItem(CourseTestCase):
Test setting due & start dates on sequential Test setting due & start dates on sequential
""" """
sequential = modulestore().get_item(self.seq_location) sequential = modulestore().get_item(self.seq_location)
self.assertIsNone(sequential.lms.due) self.assertIsNone(sequential.due)
self.client.post( self.client.post(
reverse('save_item'), reverse('save_item'),
json.dumps({ json.dumps({
...@@ -236,7 +236,7 @@ class TestEditItem(CourseTestCase): ...@@ -236,7 +236,7 @@ class TestEditItem(CourseTestCase):
content_type="application/json" content_type="application/json"
) )
sequential = modulestore().get_item(self.seq_location) sequential = modulestore().get_item(self.seq_location)
self.assertEqual(sequential.lms.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) self.assertEqual(sequential.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
self.client.post( self.client.post(
reverse('save_item'), reverse('save_item'),
json.dumps({ json.dumps({
...@@ -246,5 +246,5 @@ class TestEditItem(CourseTestCase): ...@@ -246,5 +246,5 @@ class TestEditItem(CourseTestCase):
content_type="application/json" content_type="application/json"
) )
sequential = modulestore().get_item(self.seq_location) sequential = modulestore().get_item(self.seq_location)
self.assertEqual(sequential.lms.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) self.assertEqual(sequential.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC))
self.assertEqual(sequential.lms.start, datetime.datetime(2010, 9, 12, 14, 0, tzinfo=UTC)) self.assertEqual(sequential.start, datetime.datetime(2010, 9, 12, 14, 0, tzinfo=UTC))
...@@ -2,27 +2,27 @@ import json ...@@ -2,27 +2,27 @@ import json
import logging import logging
from collections import defaultdict from collections import defaultdict
from django.http import ( HttpResponse, HttpResponseBadRequest, from django.http import (HttpResponse, HttpResponseBadRequest,
HttpResponseForbidden ) HttpResponseForbidden)
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.conf import settings from django.conf import settings
from xmodule.modulestore.exceptions import ( ItemNotFoundError, from xmodule.modulestore.exceptions import (ItemNotFoundError,
InvalidLocationError ) InvalidLocationError)
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.util.date_utils import get_default_time_display from xmodule.util.date_utils import get_default_time_display
from xblock.core import Scope from xblock.fields import Scope
from util.json_request import expect_json, JsonResponse from util.json_request import expect_json, JsonResponse
from contentstore.module_info_model import get_module_info, set_module_info from contentstore.module_info_model import get_module_info, set_module_info
from contentstore.utils import ( get_modulestore, get_lms_link_for_item, from contentstore.utils import (get_modulestore, get_lms_link_for_item,
compute_unit_state, UnitState, get_course_for_item ) compute_unit_state, UnitState, get_course_for_item)
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
...@@ -30,6 +30,7 @@ from .requests import _xmodule_recurse ...@@ -30,6 +30,7 @@ from .requests import _xmodule_recurse
from .access import has_access from .access import has_access
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xblock.plugin import PluginMissingError from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist
__all__ = ['OPEN_ENDED_COMPONENT_TYPES', __all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY', 'ADVANCED_COMPONENT_POLICY_KEY',
...@@ -91,7 +92,7 @@ def edit_subsection(request, location): ...@@ -91,7 +92,7 @@ def edit_subsection(request, location):
# we're for now assuming a single parent # we're for now assuming a single parent
if len(parent_locs) != 1: if len(parent_locs) != 1:
logging.error( logging.error(
'Multiple (or none) parents have been found for %', 'Multiple (or none) parents have been found for %s',
location location
) )
...@@ -99,12 +100,14 @@ def edit_subsection(request, location): ...@@ -99,12 +100,14 @@ def edit_subsection(request, location):
parent = modulestore().get_item(parent_locs[0]) parent = modulestore().get_item(parent_locs[0])
# remove all metadata from the generic dictionary that is presented in a # remove all metadata from the generic dictionary that is presented in a
# more normalized UI # more normalized UI. We only want to display the XBlocks fields, not
# the fields from any mixins that have been added
fields = getattr(item, 'unmixed_class', item.__class__).fields
policy_metadata = dict( policy_metadata = dict(
(field.name, field.read_from(item)) (field.name, field.read_from(item))
for field for field
in item.fields in fields.values()
if field.name not in ['display_name', 'start', 'due', 'format'] if field.name not in ['display_name', 'start', 'due', 'format']
and field.scope == Scope.settings and field.scope == Scope.settings
) )
...@@ -135,6 +138,15 @@ def edit_subsection(request, location): ...@@ -135,6 +138,15 @@ def edit_subsection(request, location):
) )
def load_mixed_class(category):
"""
Load an XBlock by category name, and apply all defined mixins
"""
component_class = XModuleDescriptor.load_class(category)
mixologist = Mixologist(settings.XBLOCK_MIXINS)
return mixologist.mix(component_class)
@login_required @login_required
def edit_unit(request, location): def edit_unit(request, location):
""" """
...@@ -163,22 +175,29 @@ def edit_unit(request, location): ...@@ -163,22 +175,29 @@ def edit_unit(request, location):
component_templates = defaultdict(list) component_templates = defaultdict(list)
for category in COMPONENT_TYPES: for category in COMPONENT_TYPES:
component_class = XModuleDescriptor.load_class(category) component_class = load_mixed_class(category)
# add the default template # add the default template
# TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington)
if hasattr(component_class, 'display_name'):
display_name = component_class.display_name.default or 'Blank'
else:
display_name = 'Blank'
component_templates[category].append(( component_templates[category].append((
component_class.display_name.default or 'Blank', display_name,
category, category,
False, # No defaults have markdown (hardcoded current default) False, # No defaults have markdown (hardcoded current default)
None # no boilerplate for overrides None # no boilerplate for overrides
)) ))
# add boilerplates # add boilerplates
for template in component_class.templates(): if hasattr(component_class, 'templates'):
component_templates[category].append(( for template in component_class.templates():
template['metadata'].get('display_name'), component_templates[category].append((
category, template['metadata'].get('display_name'),
template['metadata'].get('markdown') is not None, category,
template.get('template_id') template['metadata'].get('markdown') is not None,
)) template.get('template_id')
))
# Check if there are any advanced modules specified in the course policy. # 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 # These modules should be specified as a list of strings, where the strings
...@@ -194,7 +213,7 @@ def edit_unit(request, location): ...@@ -194,7 +213,7 @@ def edit_unit(request, location):
# class? i.e., can an advanced have more than one entry in the # class? i.e., can an advanced have more than one entry in the
# menu? one for default and others for prefilled boilerplates? # menu? one for default and others for prefilled boilerplates?
try: try:
component_class = XModuleDescriptor.load_class(category) component_class = load_mixed_class(category)
component_templates['advanced'].append(( component_templates['advanced'].append((
component_class.display_name.default or category, component_class.display_name.default or category,
...@@ -272,13 +291,17 @@ def edit_unit(request, location): ...@@ -272,13 +291,17 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link, 'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link, 'published_preview_link': lms_link,
'subsection': containing_subsection, 'subsection': containing_subsection,
'release_date': get_default_time_display(containing_subsection.lms.start) 'release_date': (
if containing_subsection.lms.start is not None else None, get_default_time_display(containing_subsection.start)
if containing_subsection.start is not None else None
),
'section': containing_section, 'section': containing_section,
'new_unit_category': 'vertical', 'new_unit_category': 'vertical',
'unit_state': unit_state, 'unit_state': unit_state,
'published_date': get_default_time_display(item.cms.published_date) 'published_date': (
if item.cms.published_date is not None else None get_default_time_display(item.published_date)
if item.published_date is not None else None
),
}) })
......
...@@ -58,13 +58,13 @@ def save_item(request): ...@@ -58,13 +58,13 @@ def save_item(request):
# 'apply' the submitted metadata, so we don't end up deleting system metadata # 'apply' the submitted metadata, so we don't end up deleting system metadata
existing_item = modulestore().get_item(item_location) existing_item = modulestore().get_item(item_location)
for metadata_key in request.POST.get('nullout', []): for metadata_key in request.POST.get('nullout', []):
_get_xblock_field(existing_item, metadata_key).write_to(existing_item, None) setattr(existing_item, metadata_key, None)
# update existing metadata with submitted metadata (which can be partial) # update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If # 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 # the intent is to make it None, use the nullout field
for metadata_key, value in request.POST.get('metadata', {}).items(): for metadata_key, value in request.POST.get('metadata', {}).items():
field = _get_xblock_field(existing_item, metadata_key) field = existing_item.fields[metadata_key]
if value is None: if value is None:
field.delete_from(existing_item) field.delete_from(existing_item)
...@@ -80,16 +80,6 @@ def save_item(request): ...@@ -80,16 +80,6 @@ def save_item(request):
return JsonResponse() return JsonResponse()
def _get_xblock_field(xblock, field_name):
"""
A temporary function to get the xblock field either from the xblock or one of its namespaces by name.
:param xblock:
:param field_name:
"""
for field in xblock.iterfields():
if field.name == field_name:
return field
@login_required @login_required
@expect_json @expect_json
def create_item(request): def create_item(request):
......
...@@ -2,6 +2,7 @@ import logging ...@@ -2,6 +2,7 @@ import logging
import sys import sys
from functools import partial from functools import partial
from django.conf import settings
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
...@@ -11,12 +12,12 @@ from xmodule_modifiers import replace_static_urls, wrap_xmodule, save_module # ...@@ -11,12 +12,12 @@ from xmodule_modifiers import replace_static_urls, wrap_xmodule, save_module #
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo import MongoUsage
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel from xblock.runtime import DbModel
from lms.xblock.field_data import lms_field_data
from util.sandboxing import can_execute_unsafe_code from util.sandboxing import can_execute_unsafe_code
import static_replace import static_replace
...@@ -97,14 +98,10 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -97,14 +98,10 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor descriptor: An XModuleDescriptor
""" """
def preview_model_data(descriptor): def preview_field_data(descriptor):
"Helper method to create a DbModel from a descriptor" "Helper method to create a DbModel from a descriptor"
return DbModel( student_data = DbModel(SessionKeyValueStore(request))
SessionKeyValueStore(request, descriptor._model_data), return lms_field_data(descriptor._field_data, student_data)
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
course_id = get_course_for_item(descriptor.location).location.course_id course_id = get_course_for_item(descriptor.location).location.course_id
...@@ -118,8 +115,9 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -118,8 +115,9 @@ def preview_module_system(request, preview_id, descriptor):
debug=True, debug=True,
replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id), replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_id=course_id),
user=request.user, user=request.user,
xblock_model_data=preview_model_data, xblock_field_data=preview_field_data,
can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)), can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
mixins=settings.XBLOCK_MIXINS,
) )
......
from xblock.runtime import KeyValueStore, InvalidScopeError """
An :class:`~xblock.runtime.KeyValueStore` that stores data in the django session
"""
from xblock.runtime import KeyValueStore
class SessionKeyValueStore(KeyValueStore): class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, descriptor_model_data): def __init__(self, request):
self._descriptor_model_data = descriptor_model_data
self._session = request.session self._session = request.session
def get(self, key): def get(self, key):
try: return self._session[tuple(key)]
return self._descriptor_model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value): def set(self, key, value):
try: self._session[tuple(key)] = value
self._descriptor_model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def delete(self, key): def delete(self, key):
try: del self._session[tuple(key)]
del self._descriptor_model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def has(self, key): def has(self, key):
return key.field_name in self._descriptor_model_data or tuple(key) in self._session return tuple(key) in self._session
...@@ -125,7 +125,7 @@ class CourseGradingModel(object): ...@@ -125,7 +125,7 @@ class CourseGradingModel(object):
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data)
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index]) return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
...@@ -144,7 +144,7 @@ class CourseGradingModel(object): ...@@ -144,7 +144,7 @@ class CourseGradingModel(object):
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data)
return cutoffs return cutoffs
...@@ -168,12 +168,12 @@ class CourseGradingModel(object): ...@@ -168,12 +168,12 @@ class CourseGradingModel(object):
grace_timedelta = timedelta(**graceperiodjson) grace_timedelta = timedelta(**graceperiodjson)
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.lms.graceperiod = grace_timedelta descriptor.graceperiod = grace_timedelta
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata) get_modulestore(course_location).update_metadata(course_location, descriptor._field_data._kvs._metadata)
@staticmethod @staticmethod
def delete_grader(course_location, index): def delete_grader(course_location, index):
...@@ -193,7 +193,7 @@ class CourseGradingModel(object): ...@@ -193,7 +193,7 @@ class CourseGradingModel(object):
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_item(course_location, descriptor._model_data._kvs._data) get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data)
@staticmethod @staticmethod
def delete_grace_period(course_location): def delete_grace_period(course_location):
...@@ -204,12 +204,12 @@ class CourseGradingModel(object): ...@@ -204,12 +204,12 @@ class CourseGradingModel(object):
course_location = Location(course_location) course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
del descriptor.lms.graceperiod del descriptor.graceperiod
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(course_location).update_metadata(course_location, descriptor._model_data._kvs._metadata) get_modulestore(course_location).update_metadata(course_location, descriptor._field_data._kvs._metadata)
@staticmethod @staticmethod
def get_section_grader_type(location): def get_section_grader_type(location):
...@@ -217,7 +217,7 @@ class CourseGradingModel(object): ...@@ -217,7 +217,7 @@ class CourseGradingModel(object):
location = Location(location) location = Location(location)
descriptor = get_modulestore(location).get_item(location) descriptor = get_modulestore(location).get_item(location)
return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded', return {"graderType": descriptor.format if descriptor.format is not None else 'Not Graded',
"location": location, "location": location,
"id": 99 # just an arbitrary value to "id": 99 # just an arbitrary value to
} }
...@@ -229,21 +229,21 @@ class CourseGradingModel(object): ...@@ -229,21 +229,21 @@ class CourseGradingModel(object):
descriptor = get_modulestore(location).get_item(location) descriptor = get_modulestore(location).get_item(location)
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded": if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
descriptor.lms.format = jsondict.get('graderType') descriptor.format = jsondict.get('graderType')
descriptor.lms.graded = True descriptor.graded = True
else: else:
del descriptor.lms.format del descriptor.format
del descriptor.lms.graded del descriptor.graded
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() descriptor.save()
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata) get_modulestore(location).update_metadata(location, descriptor._field_data._kvs._metadata)
@staticmethod @staticmethod
def convert_set_grace_period(descriptor): def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => converted to iso format # 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod rawgrace = descriptor.graceperiod
if rawgrace: if rawgrace:
hours_from_days = rawgrace.days * 24 hours_from_days = rawgrace.days * 24
seconds = rawgrace.seconds seconds = rawgrace.seconds
......
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope from xblock.fields import Scope
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from cms.xmodule_namespace import CmsBlockMixin
class CourseMetadata(object): class CourseMetadata(object):
...@@ -34,12 +35,17 @@ class CourseMetadata(object): ...@@ -34,12 +35,17 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
for field in descriptor.fields + descriptor.lms.fields: for field in descriptor.fields.values():
if field.name in CmsBlockMixin.fields:
continue
if field.scope != Scope.settings: if field.scope != Scope.settings:
continue continue
if field.name not in cls.FILTERED_LIST: if field.name in cls.FILTERED_LIST:
course[field.name] = field.read_json(descriptor) continue
course[field.name] = field.read_json(descriptor)
return course return course
...@@ -67,12 +73,8 @@ class CourseMetadata(object): ...@@ -67,12 +73,8 @@ class CourseMetadata(object):
if hasattr(descriptor, key) and getattr(descriptor, key) != val: if hasattr(descriptor, key) and getattr(descriptor, key) != val:
dirty = True dirty = True
value = getattr(CourseDescriptor, key).from_json(val) value = descriptor.fields[key].from_json(val)
setattr(descriptor, key, value) setattr(descriptor, key, value)
elif hasattr(descriptor.lms, key) and getattr(descriptor.lms, key) != key:
dirty = True
value = getattr(CourseDescriptor.lms, key).from_json(val)
setattr(descriptor.lms, key, value)
if dirty: if dirty:
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
...@@ -96,8 +98,6 @@ class CourseMetadata(object): ...@@ -96,8 +98,6 @@ class CourseMetadata(object):
for key in payload['deleteKeys']: for key in payload['deleteKeys']:
if hasattr(descriptor, key): if hasattr(descriptor, key):
delattr(descriptor, key) delattr(descriptor, key)
elif hasattr(descriptor.lms, key):
delattr(descriptor.lms, key)
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
......
...@@ -28,6 +28,10 @@ import lms.envs.common ...@@ -28,6 +28,10 @@ import lms.envs.common
from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL
from path import path from path import path
from lms.xblock.mixin import LmsBlockMixin
from cms.xmodule_namespace import CmsBlockMixin
from xmodule.modulestore.inheritance import InheritanceMixin
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
MITX_FEATURES = { MITX_FEATURES = {
...@@ -160,6 +164,13 @@ MIDDLEWARE_CLASSES = ( ...@@ -160,6 +164,13 @@ MIDDLEWARE_CLASSES = (
'ratelimitbackend.middleware.RateLimitMiddleware', 'ratelimitbackend.middleware.RateLimitMiddleware',
) )
############# XBlock Configuration ##########
# This should be moved into an XBlock Runtime/Application object
# once the responsibility of XBlock creation is moved out of modulestore - cpennington
XBLOCK_MIXINS = (LmsBlockMixin, CmsBlockMixin, InheritanceMixin)
############################ SIGNAL HANDLERS ################################ ############################ SIGNAL HANDLERS ################################
# This is imported to register the exception signal handling that logs exceptions # This is imported to register the exception signal handling that logs exceptions
import monitoring.exceptions # noqa import monitoring.exceptions # noqa
......
""" """
Module with code executed during Studio startup Module with code executed during Studio startup
""" """
import logging
from django.conf import settings from django.conf import settings
# Force settings to run so that the python path is modified # Force settings to run so that the python path is modified
...@@ -8,6 +9,8 @@ settings.INSTALLED_APPS # pylint: disable=W0104 ...@@ -8,6 +9,8 @@ settings.INSTALLED_APPS # pylint: disable=W0104
from django_startup import autostartup from django_startup import autostartup
log = logging.getLogger(__name__)
# TODO: Remove this code once Studio/CMS runs via wsgi in all environments # TODO: Remove this code once Studio/CMS runs via wsgi in all environments
INITIALIZED = False INITIALIZED = False
...@@ -22,4 +25,3 @@ def run(): ...@@ -22,4 +25,3 @@ def run():
INITIALIZED = True INITIALIZED = True
autostartup() autostartup()
...@@ -26,7 +26,6 @@ describe "Test Metadata Editor", -> ...@@ -26,7 +26,6 @@ describe "Test Metadata Editor", ->
explicitly_set: true, explicitly_set: true,
field_name: "display_name", field_name: "display_name",
help: "Specifies the name for this component.", help: "Specifies the name for this component.",
inheritable: false,
options: [], options: [],
type: CMS.Models.Metadata.GENERIC_TYPE, type: CMS.Models.Metadata.GENERIC_TYPE,
value: "Word cloud" value: "Word cloud"
...@@ -38,7 +37,6 @@ describe "Test Metadata Editor", -> ...@@ -38,7 +37,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "show_answer", field_name: "show_answer",
help: "When should you show the answer", help: "When should you show the answer",
inheritable: true,
options: [ options: [
{"display_name": "Always", "value": "always"}, {"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"}, {"display_name": "Answered", "value": "answered"},
...@@ -54,7 +52,6 @@ describe "Test Metadata Editor", -> ...@@ -54,7 +52,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "num_inputs", field_name: "num_inputs",
help: "Number of text boxes for student to input words/sentences.", help: "Number of text boxes for student to input words/sentences.",
inheritable: false,
options: {min: 1}, options: {min: 1},
type: CMS.Models.Metadata.INTEGER_TYPE, type: CMS.Models.Metadata.INTEGER_TYPE,
value: 5 value: 5
...@@ -66,7 +63,6 @@ describe "Test Metadata Editor", -> ...@@ -66,7 +63,6 @@ describe "Test Metadata Editor", ->
explicitly_set: true, explicitly_set: true,
field_name: "weight", field_name: "weight",
help: "Weight for this problem", help: "Weight for this problem",
inheritable: true,
options: {min: 1.3, max:100.2, step:0.1}, options: {min: 1.3, max:100.2, step:0.1},
type: CMS.Models.Metadata.FLOAT_TYPE, type: CMS.Models.Metadata.FLOAT_TYPE,
value: 10.2 value: 10.2
...@@ -78,7 +74,6 @@ describe "Test Metadata Editor", -> ...@@ -78,7 +74,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "list", field_name: "list",
help: "A list of things.", help: "A list of things.",
inheritable: false,
options: [], options: [],
type: CMS.Models.Metadata.LIST_TYPE, type: CMS.Models.Metadata.LIST_TYPE,
value: ["the first display value", "the second"] value: ["the first display value", "the second"]
...@@ -99,7 +94,6 @@ describe "Test Metadata Editor", -> ...@@ -99,7 +94,6 @@ describe "Test Metadata Editor", ->
explicitly_set: true, explicitly_set: true,
field_name: "unknown_type", field_name: "unknown_type",
help: "Mystery property.", help: "Mystery property.",
inheritable: false,
options: [ options: [
{"display_name": "Always", "value": "always"}, {"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"}, {"display_name": "Answered", "value": "answered"},
...@@ -145,7 +139,6 @@ describe "Test Metadata Editor", -> ...@@ -145,7 +139,6 @@ describe "Test Metadata Editor", ->
explicitly_set: false, explicitly_set: false,
field_name: "display_name", field_name: "display_name",
help: "", help: "",
inheritable: false,
options: [], options: [],
type: CMS.Models.Metadata.GENERIC_TYPE, type: CMS.Models.Metadata.GENERIC_TYPE,
value: null value: null
......
...@@ -37,21 +37,21 @@ ...@@ -37,21 +37,21 @@
<div class="field field-start-date"> <div class="field field-start-date">
<label for="start_date">${_("Release Day")}</label> <label for="start_date">${_("Release Day")}</label>
<input type="text" id="start_date" name="start_date" <input type="text" id="start_date" name="start_date"
value="${subsection.lms.start.strftime('%m/%d/%Y') if subsection.lms.start else ''}" value="${subsection.start.strftime('%m/%d/%Y') if subsection.start else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/> placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div> </div>
<div class="field field-start-time"> <div class="field field-start-time">
<label for="start_time">${_("Release Time")} (<abbr title="${_("Coordinated Universal Time")}">${_("UTC")}</abbr>)</label> <label for="start_time">${_("Release Time")} (<abbr title="${_("Coordinated Universal Time")}">${_("UTC")}</abbr>)</label>
<input type="text" id="start_time" name="start_time" <input type="text" id="start_time" name="start_time"
value="${subsection.lms.start.strftime('%H:%M') if subsection.lms.start else ''}" value="${subsection.start.strftime('%H:%M') if subsection.start else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/> placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
</div> </div>
% if subsection.lms.start and not almost_same_datetime(subsection.lms.start, parent_item.lms.start): % if subsection.start and not almost_same_datetime(subsection.start, parent_item.start):
% if parent_item.lms.start is None: % if parent_item.start is None:
<p class="notice">${_("The date above differs from the release date of {name}, which is unset.").format(name=parent_item.display_name_with_default)} <p class="notice">${_("The date above differs from the release date of {name}, which is unset.").format(name=parent_item.display_name_with_default)}
% else: % else:
<p class="notice">${_("The date above differs from the release date of {name} - {start_time}").format(name=parent_item.display_name_with_default, start_time=get_default_time_display(parent_item.lms.start))}. <p class="notice">${_("The date above differs from the release date of {name} - {start_time}").format(name=parent_item.display_name_with_default, start_time=get_default_time_display(parent_item.start))}.
% endif % endif
<a href="#" class="sync-date no-spinner">${_("Sync to {name}.").format(name=parent_item.display_name_with_default)}</a></p> <a href="#" class="sync-date no-spinner">${_("Sync to {name}.").format(name=parent_item.display_name_with_default)}</a></p>
% endif % endif
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
<div class="row gradable"> <div class="row gradable">
<label>${_("Graded as:")}</label> <label>${_("Graded as:")}</label>
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else _('Not Graded')}"> <div class="gradable-status" data-initial-status="${subsection.format if subsection.format is not None else _('Not Graded')}">
</div> </div>
<div class="due-date-input row"> <div class="due-date-input row">
...@@ -69,13 +69,13 @@ ...@@ -69,13 +69,13 @@
<div class="field field-start-date"> <div class="field field-start-date">
<label for="due_date">${_("Due Day")}</label> <label for="due_date">${_("Due Day")}</label>
<input type="text" id="due_date" name="due_date" <input type="text" id="due_date" name="due_date"
value="${subsection.lms.due.strftime('%m/%d/%Y') if subsection.lms.due else ''}" value="${subsection.due.strftime('%m/%d/%Y') if subsection.due else ''}"
placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/> placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
</div> </div>
<div class="field field-start-time"> <div class="field field-start-time">
<label for="due_time">${_("Due Time")} (<abbr title="${_('Coordinated Universal Time')}">UTC</abbr>)</label> <label for="due_time">${_("Due Time")} (<abbr title="${_('Coordinated Universal Time')}">UTC</abbr>)</label>
<input type="text" id="due_time" name="due_time" <input type="text" id="due_time" name="due_time"
value="${subsection.lms.due.strftime('%H:%M') if subsection.lms.due else ''}" value="${subsection.due.strftime('%H:%M') if subsection.due else ''}"
placeholder="HH:MM" class="time" size='10' autocomplete="off"/> placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
<a href="#" class="remove-date">${_("Remove due date")}</a> <a href="#" class="remove-date">${_("Remove due date")}</a>
......
...@@ -157,19 +157,19 @@ ...@@ -157,19 +157,19 @@
<h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3> <h3 class="section-name" data-name="${section.display_name_with_default | h}"></h3>
<div class="section-published-date"> <div class="section-published-date">
<% <%
if section.lms.start is not None: if section.start is not None:
start_date_str = section.lms.start.strftime('%m/%d/%Y') start_date_str = section.start.strftime('%m/%d/%Y')
start_time_str = section.lms.start.strftime('%H:%M') start_time_str = section.start.strftime('%H:%M')
else: else:
start_date_str = '' start_date_str = ''
start_time_str = '' start_time_str = ''
%> %>
%if section.lms.start is None: %if section.start is None:
<span class="published-status">${_("This section has not been released.")}</span> <span class="published-status">${_("This section has not been released.")}</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">${_("Schedule")}</a> <a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">${_("Schedule")}</a>
%else: %else:
<span class="published-status"><strong>${_("Will Release:")}</strong> <span class="published-status"><strong>${_("Will Release:")}</strong>
${date_utils.get_default_time_display(section.lms.start)}</span> ${date_utils.get_default_time_display(section.start)}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" <a href="#" class="edit-button" data-date="${start_date_str}"
data-time="${start_time_str}" data-id="${section.location}">${_("Edit")}</a> data-time="${start_time_str}" data-id="${section.location}">${_("Edit")}</a>
%endif %endif
...@@ -199,7 +199,7 @@ ...@@ -199,7 +199,7 @@
</a> </a>
</div> </div>
<div class="gradable-status" data-initial-status="${subsection.lms.format if subsection.lms.format is not None else _('Not Graded')}"> <div class="gradable-status" data-initial-status="${subsection.format if subsection.format is not None else _('Not Graded')}">
</div> </div>
<div class="item-actions"> <div class="item-actions">
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
<li> <li>
<ol id="sortable"> <ol id="sortable">
% for child in module.get_children(): % for child in module.get_children():
<li class="${module.category}"> <li class="${module.scope_ids.block_type}">
<a href="#" class="module-edit" <a href="#" class="module-edit"
data-id="${child.location.url()}" data-id="${child.location.url()}"
data-type="${child.js_module_name}" data-type="${child.js_module_name}"
......
...@@ -21,7 +21,7 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -21,7 +21,7 @@ This def will enumerate through a passed in subsection and list all of the units
%> %>
<div class="section-item ${selected_class}"> <div class="section-item ${selected_class}">
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item"> <a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
<span class="${unit.category}-icon"></span> <span class="${unit.scope_ids.block_type}-icon"></span>
<span class="unit-name">${unit.display_name_with_default}</span> <span class="unit-name">${unit.display_name_with_default}</span>
</a> </a>
% if actions: % if actions:
......
...@@ -4,12 +4,12 @@ Namespace defining common fields used by Studio for all blocks ...@@ -4,12 +4,12 @@ Namespace defining common fields used by Studio for all blocks
import datetime import datetime
from xblock.core import Namespace, Scope, ModelType, String from xblock.fields import Scope, Field, Integer, XBlockMixin
class DateTuple(ModelType): class DateTuple(Field):
""" """
ModelType that stores datetime objects as time tuples Field that stores datetime objects as time tuples
""" """
def from_json(self, value): def from_json(self, value):
return datetime.datetime(*value[0:6]) return datetime.datetime(*value[0:6])
...@@ -21,9 +21,9 @@ class DateTuple(ModelType): ...@@ -21,9 +21,9 @@ class DateTuple(ModelType):
return list(value.timetuple()) return list(value.timetuple())
class CmsNamespace(Namespace): class CmsBlockMixin(XBlockMixin):
""" """
Namespace with fields common to all blocks in Studio Mixin with fields common to all blocks in Studio
""" """
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings) published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
published_by = String(help="Id of the user who published this module", scope=Scope.settings) published_by = Integer(help="Id of the user who published this module", scope=Scope.settings)
...@@ -47,7 +47,7 @@ from ratelimitbackend.exceptions import RateLimitException ...@@ -47,7 +47,7 @@ from ratelimitbackend.exceptions import RateLimitException
import student.views as student_views import student.views as student_views
# Required for Pearson # Required for Pearson
from courseware.views import get_module_for_descriptor, jump_to from courseware.views import get_module_for_descriptor, jump_to
from courseware.model_data import ModelDataCache from courseware.model_data import FieldDataCache
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -944,7 +944,7 @@ def test_center_login(request): ...@@ -944,7 +944,7 @@ def test_center_login(request):
log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location)) log.error("cand {} on exam {} for course {}: descriptor not found for location {}".format(client_candidate_id, exam_series_code, course_id, location))
return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram")) return HttpResponseRedirect(makeErrorURL(error_url, "missingClientProgram"))
timelimit_module_cache = ModelDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user, timelimit_module_cache = FieldDataCache.cache_for_descriptor_descendents(course_id, testcenteruser.user,
timelimit_descriptor, depth=None) timelimit_descriptor, depth=None)
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor, timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache, course_id, position=None) timelimit_module_cache, course_id, position=None)
......
...@@ -32,7 +32,7 @@ class TestXmoduleModfiers(ModuleStoreTestCase): ...@@ -32,7 +32,7 @@ class TestXmoduleModfiers(ModuleStoreTestCase):
late_problem = ItemFactory.create( late_problem = ItemFactory.create(
parent_location=section.location, display_name='problem hist 2', parent_location=section.location, display_name='problem hist 2',
category='problem') category='problem')
late_problem.lms.start = datetime.datetime.now(UTC) + datetime.timedelta(days=32) late_problem.start = datetime.datetime.now(UTC) + datetime.timedelta(days=32)
late_problem.has_score = False late_problem.has_score = False
......
...@@ -29,12 +29,16 @@ def wrap_xmodule(get_html, module, template, context=None): ...@@ -29,12 +29,16 @@ def wrap_xmodule(get_html, module, template, context=None):
if context is None: if context is None:
context = {} context = {}
# If XBlock generated this class, then use the first baseclass
# as the name (since that's the original, unmixed class)
class_name = getattr(module, 'unmixed_class', module.__class__).__name__
@wraps(get_html) @wraps(get_html)
def _get_html(): def _get_html():
context.update({ context.update({
'content': get_html(), 'content': get_html(),
'display_name': module.display_name, 'display_name': module.display_name,
'class_': module.__class__.__name__, 'class_': class_name,
'module_name': module.js_module_name 'module_name': module.js_module_name
}) })
...@@ -157,7 +161,7 @@ def add_histogram(get_html, module, user): ...@@ -157,7 +161,7 @@ def add_histogram(get_html, module, user):
# doesn't like symlinks) # doesn't like symlinks)
filepath = filename filepath = filename
data_dir = osfs.root_path.rsplit('/')[-1] data_dir = osfs.root_path.rsplit('/')[-1]
giturl = module.lms.giturl or 'https://github.com/MITx' giturl = module.giturl or 'https://github.com/MITx'
edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath) edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath)
else: else:
edit_link = False edit_link = False
...@@ -165,22 +169,21 @@ def add_histogram(get_html, module, user): ...@@ -165,22 +169,21 @@ def add_histogram(get_html, module, user):
giturl = "" giturl = ""
data_dir = "" data_dir = ""
source_file = module.lms.source_file # source used to generate the problem XML, eg latex or word source_file = module.source_file # source used to generate the problem XML, eg latex or word
# useful to indicate to staff if problem has been released or not # useful to indicate to staff if problem has been released or not
# TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here # TODO (ichuang): use _has_access_descriptor.can_load in lms.courseware.access, instead of now>mstart comparison here
now = datetime.datetime.now(UTC()) now = datetime.datetime.now(UTC())
is_released = "unknown" is_released = "unknown"
mstart = module.descriptor.lms.start mstart = module.descriptor.start
if mstart is not None: if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>" is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields], staff_context = {'fields': [(name, field.read_from(module)) for name, field in module.fields.items()],
'lms_fields': [(field.name, getattr(module.lms, field.name)) for field in module.lms.fields], 'xml_attributes': getattr(module.descriptor, 'xml_attributes', {}),
'xml_attributes' : getattr(module.descriptor, 'xml_attributes', {}),
'location': module.location, 'location': module.location,
'xqa_key': module.lms.xqa_key, 'xqa_key': module.xqa_key,
'source_file': source_file, 'source_file': source_file,
'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file), 'source_url': '%s/%s/tree/master/%s' % (giturl, data_dir, source_file),
'category': str(module.__class__.__name__), 'category': str(module.__class__.__name__),
......
...@@ -6,7 +6,7 @@ from xmodule.x_module import XModule ...@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError from xmodule.exceptions import InvalidDefinitionError
from xblock.core import String, Scope, Dict from xblock.fields import String, Scope, Dict
DEFAULT = "_DEFAULT_GROUP" DEFAULT = "_DEFAULT_GROUP"
......
...@@ -5,7 +5,7 @@ from pkg_resources import resource_string ...@@ -5,7 +5,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String from xblock.fields import Scope, String
import textwrap import textwrap
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -18,7 +18,7 @@ from .progress import Progress ...@@ -18,7 +18,7 @@ from .progress import Progress
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.core import Scope, String, Boolean, Dict, Integer, Float from xblock.fields import Scope, String, Boolean, Dict, Integer, Float
from .fields import Timedelta, Date from .fields import Timedelta, Date
from django.utils.timezone import UTC from django.utils.timezone import UTC
......
...@@ -5,7 +5,7 @@ from pkg_resources import resource_string ...@@ -5,7 +5,7 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from xblock.core import Integer, Scope, String, List, Float, Boolean from xblock.fields import Integer, Scope, String, List, Float, Boolean
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from .fields import Date, Timedelta from .fields import Date, Timedelta
......
...@@ -10,7 +10,7 @@ from pkg_resources import resource_string ...@@ -10,7 +10,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xblock.core import Scope, List from xblock.fields import Scope, List
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
......
...@@ -13,7 +13,7 @@ from xmodule.util.decorators import lazyproperty ...@@ -13,7 +13,7 @@ from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf from xmodule.graders import grader_from_conf
import json import json
from xblock.core import Scope, List, String, Dict, Boolean from xblock.fields import Scope, List, String, Dict, Boolean
from .fields import Date from .fields import Date
from xmodule.modulestore.locator import CourseLocator from xmodule.modulestore.locator import CourseLocator
from django.utils.timezone import UTC from django.utils.timezone import UTC
...@@ -118,6 +118,13 @@ class Textbook(object): ...@@ -118,6 +118,13 @@ class Textbook(object):
return table_of_contents return table_of_contents
def __eq__(self, other):
return (self.title == other.title and
self.book_url == other.book_url)
def __ne__(self, other):
return not self == other
class TextbookList(List): class TextbookList(List):
def from_json(self, values): def from_json(self, values):
...@@ -737,7 +744,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -737,7 +744,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
all_descriptors - This contains a list of all xmodules that can all_descriptors - This contains a list of all xmodules that can
effect grading a student. This is used to efficiently fetch effect grading a student. This is used to efficiently fetch
all the xmodule state for a ModelDataCache without walking all the xmodule state for a FieldDataCache without walking
the descriptor tree again. the descriptor tree again.
...@@ -754,14 +761,14 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -754,14 +761,14 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
for c in self.get_children(): for c in self.get_children():
for s in c.get_children(): for s in c.get_children():
if s.lms.graded: if s.graded:
xmoduledescriptors = list(yield_descriptor_descendents(s)) xmoduledescriptors = list(yield_descriptor_descendents(s))
xmoduledescriptors.append(s) xmoduledescriptors.append(s)
# The xmoduledescriptors included here are only the ones that have scores. # The xmoduledescriptors included here are only the ones that have scores.
section_description = {'section_descriptor': s, 'xmoduledescriptors': filter(lambda child: child.has_score, xmoduledescriptors)} section_description = {'section_descriptor': s, 'xmoduledescriptors': filter(lambda child: child.has_score, xmoduledescriptors)}
section_format = s.lms.format if s.lms.format is not None else '' section_format = s.format if s.format is not None else ''
graded_sections[section_format] = graded_sections.get(section_format, []) + [section_description] graded_sections[section_format] = graded_sections.get(section_format, []) + [section_description]
all_descriptors.extend(xmoduledescriptors) all_descriptors.extend(xmoduledescriptors)
......
...@@ -15,7 +15,7 @@ from lxml import etree ...@@ -15,7 +15,7 @@ from lxml import etree
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xblock.core import Scope, String, Integer, Boolean, Dict, List from xblock.fields import Scope, String, Integer, Boolean, Dict, List
from capa.responsetypes import FormulaResponse from capa.responsetypes import FormulaResponse
......
...@@ -3,7 +3,7 @@ from pkg_resources import resource_string ...@@ -3,7 +3,7 @@ from pkg_resources import resource_string
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xblock.core import String, Scope from xblock.fields import String, Scope
from uuid import uuid4 from uuid import uuid4
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
from pkg_resources import resource_string from pkg_resources import resource_string
from xmodule.mako_module import MakoModuleDescriptor from xmodule.mako_module import MakoModuleDescriptor
from xblock.core import Scope, String from xblock.fields import Scope, String
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -12,7 +12,8 @@ from lxml import etree ...@@ -12,7 +12,8 @@ from lxml import etree
from xmodule.x_module import XModule, XModuleDescriptor from xmodule.x_module import XModule, XModuleDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xblock.core import String, Scope from xblock.fields import String, Scope, ScopeIds
from xblock.field_data import DictFieldData
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -95,16 +96,19 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor): ...@@ -95,16 +96,19 @@ class ErrorDescriptor(ErrorFields, XModuleDescriptor):
) )
# real metadata stays in the content, but add a display name # real metadata stays in the content, but add a display name
model_data = { field_data = DictFieldData({
'error_msg': str(error_msg), 'error_msg': str(error_msg),
'contents': contents, 'contents': contents,
'display_name': 'Error: ' + location.url(), 'display_name': 'Error: ' + location.url(),
'location': location, 'location': location,
'category': 'error' 'category': 'error'
} })
return cls( return system.construct_xblock_from_class(
system, cls,
model_data, field_data,
# The error module doesn't use scoped data, and thus doesn't need
# real scope keys
ScopeIds('error', None, location, location)
) )
def get_context(self): def get_context(self):
......
...@@ -2,7 +2,7 @@ import time ...@@ -2,7 +2,7 @@ import time
import logging import logging
import re import re
from xblock.core import ModelType from xblock.fields import Field
import datetime import datetime
import dateutil.parser import dateutil.parser
...@@ -11,7 +11,7 @@ from pytz import UTC ...@@ -11,7 +11,7 @@ from pytz import UTC
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Date(ModelType): class Date(Field):
''' '''
Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes. Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes.
''' '''
...@@ -20,6 +20,8 @@ class Date(ModelType): ...@@ -20,6 +20,8 @@ class Date(ModelType):
PREVENT_DEFAULT_DAY_MON_SEED1 = datetime.datetime(CURRENT_YEAR, 1, 1, tzinfo=UTC) PREVENT_DEFAULT_DAY_MON_SEED1 = datetime.datetime(CURRENT_YEAR, 1, 1, tzinfo=UTC)
PREVENT_DEFAULT_DAY_MON_SEED2 = datetime.datetime(CURRENT_YEAR, 2, 2, tzinfo=UTC) PREVENT_DEFAULT_DAY_MON_SEED2 = datetime.datetime(CURRENT_YEAR, 2, 2, tzinfo=UTC)
MUTABLE = False
def _parse_date_wo_default_month_day(self, field): def _parse_date_wo_default_month_day(self, field):
""" """
Parse the field as an iso string but prevent dateutils from defaulting the day or month while Parse the field as an iso string but prevent dateutils from defaulting the day or month while
...@@ -76,12 +78,12 @@ class Date(ModelType): ...@@ -76,12 +78,12 @@ class Date(ModelType):
else: else:
return value.isoformat() return value.isoformat()
else: else:
raise TypeError("Cannot convert {} to json".format(value)) raise TypeError("Cannot convert {!r} to json".format(value))
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$') TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
class Timedelta(ModelType): class Timedelta(Field):
# Timedeltas are immutable, see http://docs.python.org/2/library/datetime.html#available-types # Timedeltas are immutable, see http://docs.python.org/2/library/datetime.html#available-types
MUTABLE = False MUTABLE = False
......
...@@ -6,7 +6,7 @@ from pkg_resources import resource_string ...@@ -6,7 +6,7 @@ from pkg_resources import resource_string
from xmodule.editing_module import EditingDescriptor from xmodule.editing_module import EditingDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, Integer, String from xblock.fields import Scope, Integer, String
from .fields import Date from .fields import Date
......
...@@ -14,7 +14,7 @@ from xmodule.xml_module import XmlDescriptor ...@@ -14,7 +14,7 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
from pkg_resources import resource_string from pkg_resources import resource_string
from xblock.core import String, Scope from xblock.fields import String, Scope
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -7,7 +7,7 @@ from lxml import etree ...@@ -7,7 +7,7 @@ from lxml import etree
from path import path from path import path
from pkg_resources import resource_string from pkg_resources import resource_string
from xblock.core import Scope, String from xblock.fields import Scope, String
from xmodule.editing_module import EditingDescriptor from xmodule.editing_module import EditingDescriptor
from xmodule.html_checker import check_html from xmodule.html_checker import check_html
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
......
...@@ -2,10 +2,8 @@ from .x_module import XModuleDescriptor, DescriptorSystem ...@@ -2,10 +2,8 @@ from .x_module import XModuleDescriptor, DescriptorSystem
class MakoDescriptorSystem(DescriptorSystem): class MakoDescriptorSystem(DescriptorSystem):
def __init__(self, load_item, resources_fs, error_tracker, def __init__(self, render_template, **kwargs):
render_template, **kwargs): super(MakoDescriptorSystem, self).__init__(**kwargs)
super(MakoDescriptorSystem, self).__init__(
load_item, resources_fs, error_tracker, **kwargs)
self.render_template = render_template self.render_template = render_template
......
...@@ -398,7 +398,7 @@ class ModuleStoreBase(ModuleStore): ...@@ -398,7 +398,7 @@ class ModuleStoreBase(ModuleStore):
''' '''
Implement interface functionality that can be shared. Implement interface functionality that can be shared.
''' '''
def __init__(self, metadata_inheritance_cache_subsystem=None, request_cache=None, modulestore_update_signal=None): def __init__(self, metadata_inheritance_cache_subsystem=None, request_cache=None, modulestore_update_signal=None, xblock_mixins=()):
''' '''
Set up the error-tracking logic. Set up the error-tracking logic.
''' '''
...@@ -406,6 +406,7 @@ class ModuleStoreBase(ModuleStore): ...@@ -406,6 +406,7 @@ class ModuleStoreBase(ModuleStore):
self.metadata_inheritance_cache_subsystem = metadata_inheritance_cache_subsystem self.metadata_inheritance_cache_subsystem = metadata_inheritance_cache_subsystem
self.modulestore_update_signal = modulestore_update_signal self.modulestore_update_signal = modulestore_update_signal
self.request_cache = request_cache self.request_cache = request_cache
self.xblock_mixins = xblock_mixins
def _get_errorlog(self, location): def _get_errorlog(self, location):
""" """
......
...@@ -62,6 +62,7 @@ def create_modulestore_instance(engine, options): ...@@ -62,6 +62,7 @@ def create_modulestore_instance(engine, options):
metadata_inheritance_cache_subsystem=metadata_inheritance_cache, metadata_inheritance_cache_subsystem=metadata_inheritance_cache,
request_cache=request_cache, request_cache=request_cache,
modulestore_update_signal=Signal(providing_args=['modulestore', 'course_id', 'location']), modulestore_update_signal=Signal(providing_args=['modulestore', 'course_id', 'location']),
xblock_mixins=getattr(settings, 'XBLOCK_MIXINS', ()),
**_options **_options
) )
......
from xblock.core import Scope from datetime import datetime
from pytz import UTC
# A list of metadata that this module can inherit from its parent module
INHERITABLE_METADATA = ( from xblock.fields import Scope, Boolean, String, Float, XBlockMixin
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize', from xmodule.fields import Date, Timedelta
# TODO (ichuang): used for Fall 2012 xqa server access from xblock.runtime import KeyValueStore
'xqa_key',
# How many days early to show a course element to beta testers (float)
# intended to be set per-course, but can be overridden in for specific class InheritanceMixin(XBlockMixin):
# elements. Can be a float. """Field definitions for inheritable fields"""
'days_early_for_beta',
'giturl', # for git edit link graded = Boolean(
'static_asset_path', # for static assets placed outside xcontent contentstore help="Whether this module contributes to the final course grade",
) default=False,
scope=Scope.settings
)
start = Date(
help="Start time when this module is visible",
default=datetime.fromtimestamp(0, UTC),
scope=Scope.settings
)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
giturl = String(help="url root for course data git repository", scope=Scope.settings)
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
graceperiod = Timedelta(
help="Amount of time after the due date that submissions will be accepted",
scope=Scope.settings
)
showanswer = String(
help="When to show the problem answer to the student",
scope=Scope.settings,
default="finished"
)
rerandomize = String(
help="When to rerandomize the problem",
default="never",
scope=Scope.settings
)
days_early_for_beta = Float(
help="Number of days early to show content to beta users",
default=None,
scope=Scope.settings
)
static_asset_path = String(help="Path to use for static assets - overrides Studio c4x://", scope=Scope.settings, default='')
def compute_inherited_metadata(descriptor): def compute_inherited_metadata(descriptor):
...@@ -21,59 +52,69 @@ def compute_inherited_metadata(descriptor): ...@@ -21,59 +52,69 @@ def compute_inherited_metadata(descriptor):
NOTE: This means that there is no such thing as lazy loading at the NOTE: This means that there is no such thing as lazy loading at the
moment--this accesses all the children.""" moment--this accesses all the children."""
for child in descriptor.get_children(): if descriptor.has_children:
inherit_metadata(child, descriptor._model_data) parent_metadata = descriptor.xblock_kvs.inherited_settings.copy()
compute_inherited_metadata(child) # add any of descriptor's explicitly set fields to the inheriting list
for field in InheritanceMixin.fields.values():
# pylint: disable = W0212
if descriptor._field_data.has(descriptor, field.name):
# inherited_settings values are json repr
parent_metadata[field.name] = field.read_json(descriptor)
for child in descriptor.get_children():
inherit_metadata(child, parent_metadata)
compute_inherited_metadata(child)
def inherit_metadata(descriptor, model_data):
def inherit_metadata(descriptor, inherited_data):
""" """
Updates this module with metadata inherited from a containing module. Updates this module with metadata inherited from a containing module.
Only metadata specified in self.inheritable_metadata will Only metadata specified in self.inheritable_metadata will
be inherited be inherited
"""
# The inherited values that are actually being used.
if not hasattr(descriptor, '_inherited_metadata'):
setattr(descriptor, '_inherited_metadata', {})
# All inheritable metadata values (for which a value exists in model_data). `inherited_data`: A dictionary mapping field names to the values that
if not hasattr(descriptor, '_inheritable_metadata'): they should inherit
setattr(descriptor, '_inheritable_metadata', {}) """
try:
# Set all inheritable metadata from kwargs that are descriptor.xblock_kvs.inherited_settings = inherited_data
# in self.inheritable_metadata and aren't already set in metadata except AttributeError: # the kvs doesn't have inherited_settings probably b/c it's an error module
for attr in INHERITABLE_METADATA: pass
if attr in model_data:
descriptor._inheritable_metadata[attr] = model_data[attr]
if attr not in descriptor._model_data:
descriptor._inherited_metadata[attr] = model_data[attr]
descriptor._model_data[attr] = model_data[attr]
def own_metadata(module): def own_metadata(module):
# IN SPLIT MONGO this is just ['metadata'] as it keeps ['_inherited_metadata'] separate!
# FIXME move into kvs? will that work for xml mongo?
""" """
Return a dictionary that contains only non-inherited field keys, Return a dictionary that contains only non-inherited field keys,
mapped to their values mapped to their serialized values
""" """
inherited_metadata = getattr(module, '_inherited_metadata', {}) return module.get_explicitly_set_fields_by_scope(Scope.settings)
metadata = {}
for field in module.fields + module.lms.fields: class InheritanceKeyValueStore(KeyValueStore):
# Only save metadata that wasn't inherited """
if field.scope != Scope.settings: Common superclass for kvs's which know about inheritance of settings. Offers simple
continue dict-based storage of fields and lookup of inherited values.
if field.name in inherited_metadata and module._model_data.get(field.name) == inherited_metadata.get(field.name): Note: inherited_settings is a dict of key to json values (internal xblock field repr)
continue """
def __init__(self, initial_values=None, inherited_settings=None):
if field.name not in module._model_data: super(InheritanceKeyValueStore, self).__init__()
continue self.inherited_settings = inherited_settings or {}
self._fields = initial_values or {}
try:
metadata[field.name] = module._model_data[field.name] def get(self, key):
except KeyError: return self._fields[key.field_name]
# Ignore any missing keys in _model_data
pass def set(self, key, value):
# xml backed courses are read-only, but they do have some computed fields
return metadata self._fields[key.field_name] = value
def delete(self, key):
del self._fields[key.field_name]
def has(self, key):
return key.field_name in self._fields
def default(self, key):
"""
Check to see if the default should be from inheritance rather than from the field's global default
"""
return self.inherited_settings[key.field_name]
...@@ -6,7 +6,6 @@ from __future__ import absolute_import ...@@ -6,7 +6,6 @@ from __future__ import absolute_import
import logging import logging
import inspect import inspect
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from urllib import quote
from bson.objectid import ObjectId from bson.objectid import ObjectId
from bson.errors import InvalidId from bson.errors import InvalidId
...@@ -19,6 +18,15 @@ from .parsers import BRANCH_PREFIX, BLOCK_PREFIX, URL_VERSION_PREFIX ...@@ -19,6 +18,15 @@ from .parsers import BRANCH_PREFIX, BLOCK_PREFIX, URL_VERSION_PREFIX
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class LocalId(object):
"""
Class for local ids for non-persisted xblocks
Should be hashable and distinguishable, but nothing else
"""
pass
class Locator(object): class Locator(object):
""" """
A locator is like a URL, it refers to a course resource. A locator is like a URL, it refers to a course resource.
...@@ -386,9 +394,12 @@ class BlockUsageLocator(CourseLocator): ...@@ -386,9 +394,12 @@ class BlockUsageLocator(CourseLocator):
self.set_property('usage_id', new) self.set_property('usage_id', new)
def init_block_ref(self, block_ref): def init_block_ref(self, block_ref):
parse = parse_block_ref(block_ref) if isinstance(block_ref, LocalId):
assert parse, 'Could not parse "%s" as a block_ref' % block_ref self.set_usage_id(block_ref)
self.set_usage_id(parse['block']) else:
parse = parse_block_ref(block_ref)
assert parse, 'Could not parse "%s" as a block_ref' % block_ref
self.set_usage_id(parse['block'])
def init_block_ref_from_url(self, url): def init_block_ref_from_url(self, url):
if isinstance(url, Locator): if isinstance(url, Locator):
...@@ -409,12 +420,8 @@ class BlockUsageLocator(CourseLocator): ...@@ -409,12 +420,8 @@ class BlockUsageLocator(CourseLocator):
""" """
Return a string representing this location. Return a string representing this location.
""" """
rep = CourseLocator.__unicode__(self) rep = super(BlockUsageLocator, self).__unicode__()
if self.usage_id is None: return rep + BLOCK_PREFIX + unicode(self.usage_id)
# usage_id has not been initialized
return rep + BLOCK_PREFIX + 'NONE'
else:
return rep + BLOCK_PREFIX + self.usage_id
class DescriptionLocator(Locator): class DescriptionLocator(Locator):
......
...@@ -29,9 +29,9 @@ class MixedModuleStore(ModuleStoreBase): ...@@ -29,9 +29,9 @@ class MixedModuleStore(ModuleStoreBase):
if 'default' not in stores: if 'default' not in stores:
raise Exception('Missing a default modulestore in the MixedModuleStore __init__ method.') raise Exception('Missing a default modulestore in the MixedModuleStore __init__ method.')
for key in stores: for key, store in stores.items():
self.modulestores[key] = create_modulestore_instance(stores[key]['ENGINE'], self.modulestores[key] = create_modulestore_instance(store['ENGINE'],
stores[key]['OPTIONS']) store['OPTIONS'])
def _get_modulestore_for_courseid(self, course_id): def _get_modulestore_for_courseid(self, course_id):
""" """
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Provide names as exported by older mongo.py module Provide names as exported by older mongo.py module
""" """
from xmodule.modulestore.mongo.base import MongoModuleStore, MongoKeyValueStore, MongoUsage from xmodule.modulestore.mongo.base import MongoModuleStore, MongoKeyValueStore
# Backwards compatibility for prod systems that refererence # Backwards compatibility for prod systems that refererence
# xmodule.modulestore.mongo.DraftMongoModuleStore # xmodule.modulestore.mongo.DraftMongoModuleStore
......
...@@ -42,7 +42,7 @@ def wrap_draft(item): ...@@ -42,7 +42,7 @@ def wrap_draft(item):
non-draft location in either case non-draft location in either case
""" """
setattr(item, 'is_draft', item.location.revision == DRAFT) setattr(item, 'is_draft', item.location.revision == DRAFT)
item.location = item.location.replace(revision=None) item.scope_ids = item.scope_ids._replace(usage_id=item.location.replace(revision=None))
return item return item
...@@ -235,10 +235,10 @@ class DraftModuleStore(MongoModuleStore): ...@@ -235,10 +235,10 @@ class DraftModuleStore(MongoModuleStore):
""" """
draft = self.get_item(location) draft = self.get_item(location)
draft.cms.published_date = datetime.now(UTC) draft.published_date = datetime.now(UTC)
draft.cms.published_by = published_by_id draft.published_by = published_by_id
super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data) super(DraftModuleStore, self).update_item(location, draft._field_data._kvs._data)
super(DraftModuleStore, self).update_children(location, draft._model_data._kvs._children) super(DraftModuleStore, self).update_children(location, draft._field_data._kvs._children)
super(DraftModuleStore, self).update_metadata(location, own_metadata(draft)) super(DraftModuleStore, self).update_metadata(location, own_metadata(draft))
self.delete_item(location) self.delete_item(location)
......
...@@ -2,15 +2,17 @@ import sys ...@@ -2,15 +2,17 @@ import sys
import logging import logging
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator, LocalId
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xblock.runtime import DbModel from xblock.runtime import DbModel
from ..exceptions import ItemNotFoundError from ..exceptions import ItemNotFoundError
from .split_mongo_kvs import SplitMongoKVS, SplitMongoKVSid from .split_mongo_kvs import SplitMongoKVS
from xblock.fields import ScopeIds
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CachingDescriptorSystem(MakoDescriptorSystem): class CachingDescriptorSystem(MakoDescriptorSystem):
""" """
A system that has a cache of a course version's json that it will use to load modules A system that has a cache of a course version's json that it will use to load modules
...@@ -18,8 +20,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -18,8 +20,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
Computes the settings (nee 'metadata') inheritance upon creation. Computes the settings (nee 'metadata') inheritance upon creation.
""" """
def __init__(self, modulestore, course_entry, module_data, lazy, def __init__(self, modulestore, course_entry, default_class, module_data, lazy, **kwargs):
default_class, error_tracker, render_template):
""" """
Computes the settings inheritance and sets up the cache. Computes the settings inheritance and sets up the cache.
...@@ -28,34 +29,31 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -28,34 +29,31 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
module_data: a dict mapping Location -> json that was cached from the module_data: a dict mapping Location -> json that was cached from the
underlying modulestore underlying modulestore
default_class: The default_class to use when loading an
XModuleDescriptor from the module_data
resources_fs: a filesystem, as per MakoDescriptorSystem
error_tracker: a function that logs errors for later display to users
render_template: a function for rendering templates, as per
MakoDescriptorSystem
""" """
# TODO find all references to resources_fs and make handle None # TODO find all references to resources_fs and make handle None
super(CachingDescriptorSystem, self).__init__( super(CachingDescriptorSystem, self).__init__(load_item=self._load_item, **kwargs)
self._load_item, None, error_tracker, render_template)
self.modulestore = modulestore self.modulestore = modulestore
self.course_entry = course_entry self.course_entry = course_entry
self.lazy = lazy self.lazy = lazy
self.module_data = module_data self.module_data = module_data
self.default_class = default_class
# TODO see if self.course_id is needed: is already in course_entry but could be > 1 value # TODO see if self.course_id is needed: is already in course_entry but could be > 1 value
# Compute inheritance # Compute inheritance
modulestore.inherit_settings( modulestore.inherit_settings(
course_entry.get('blocks', {}), course_entry.get('blocks', {}),
course_entry.get('blocks', {}).get(course_entry.get('root')) course_entry.get('blocks', {}).get(course_entry.get('root'))
) )
self.default_class = default_class
self.local_modules = {}
def _load_item(self, usage_id, course_entry_override=None): def _load_item(self, usage_id, course_entry_override=None):
# TODO ensure all callers of system.load_item pass just the id # TODO ensure all callers of system.load_item pass just the id
if isinstance(usage_id, BlockUsageLocator) and isinstance(usage_id.usage_id, LocalId):
try:
return self.local_modules[usage_id]
except KeyError:
raise ItemNotFoundError
json_data = self.module_data.get(usage_id) json_data = self.module_data.get(usage_id)
if json_data is None: if json_data is None:
# deeper than initial descendant fetch or doesn't exist # deeper than initial descendant fetch or doesn't exist
...@@ -75,6 +73,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -75,6 +73,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
course_entry_override = self.course_entry course_entry_override = self.course_entry
# most likely a lazy loader or the id directly # most likely a lazy loader or the id directly
definition = json_data.get('definition', {}) definition = json_data.get('definition', {})
definition_id = self.modulestore.definition_locator(definition)
# If no usage id is provided, generate an in-memory id
if usage_id is None:
usage_id = LocalId()
block_locator = BlockUsageLocator( block_locator = BlockUsageLocator(
version_guid=course_entry_override['_id'], version_guid=course_entry_override['_id'],
...@@ -87,25 +90,24 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -87,25 +90,24 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
definition, definition,
json_data.get('fields', {}), json_data.get('fields', {}),
json_data.get('_inherited_settings'), json_data.get('_inherited_settings'),
block_locator, )
json_data.get('category')) field_data = DbModel(kvs)
model_data = DbModel(kvs, class_, None,
SplitMongoKVSid(
# DbModel req's that these support .url()
block_locator,
self.modulestore.definition_locator(definition)))
try: try:
module = class_(self, model_data) module = self.construct_xblock_from_class(
class_,
field_data,
ScopeIds(None, json_data.get('category'), definition_id, block_locator)
)
except Exception: except Exception:
log.warning("Failed to load descriptor", exc_info=True) log.warning("Failed to load descriptor", exc_info=True)
if usage_id is None:
usage_id = "MISSING"
return ErrorDescriptor.from_json( return ErrorDescriptor.from_json(
json_data, json_data,
self, self,
BlockUsageLocator(version_guid=course_entry_override['_id'], BlockUsageLocator(
usage_id=usage_id), version_guid=course_entry_override['_id'],
usage_id=usage_id
),
error_msg=exc_info_to_str(sys.exc_info()) error_msg=exc_info_to_str(sys.exc_info())
) )
...@@ -117,4 +119,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -117,4 +119,9 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
module.definition_locator = self.modulestore.definition_locator(definition) module.definition_locator = self.modulestore.definition_locator(definition)
# decache any pending field settings # decache any pending field settings
module.save() module.save()
# If this is an in-memory block, store it in this system
if isinstance(block_locator.usage_id, LocalId):
self.local_modules[block_locator] = module
return module return module
...@@ -8,14 +8,15 @@ from path import path ...@@ -8,14 +8,15 @@ from path import path
from xmodule.errortracker import null_error_tracker from xmodule.errortracker import null_error_tracker
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.locator import BlockUsageLocator, DescriptionLocator, CourseLocator, VersionTree from xmodule.modulestore.locator import BlockUsageLocator, DescriptionLocator, CourseLocator, VersionTree, LocalId
from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError
from xmodule.modulestore import inheritance, ModuleStoreBase from xmodule.modulestore import inheritance, ModuleStoreBase
from ..exceptions import ItemNotFoundError from ..exceptions import ItemNotFoundError
from .definition_lazy_loader import DefinitionLazyLoader from .definition_lazy_loader import DefinitionLazyLoader
from .caching_descriptor_system import CachingDescriptorSystem from .caching_descriptor_system import CachingDescriptorSystem
from xblock.core import Scope from xblock.fields import Scope
from xblock.runtime import Mixologist
from pytz import UTC from pytz import UTC
import collections import collections
...@@ -41,6 +42,8 @@ log = logging.getLogger(__name__) ...@@ -41,6 +42,8 @@ log = logging.getLogger(__name__)
#============================================================================== #==============================================================================
class SplitMongoModuleStore(ModuleStoreBase): class SplitMongoModuleStore(ModuleStoreBase):
""" """
A Mongodb backed ModuleStore supporting versions, inheritance, A Mongodb backed ModuleStore supporting versions, inheritance,
...@@ -53,7 +56,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -53,7 +56,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
mongo_options=None, mongo_options=None,
**kwargs): **kwargs):
ModuleStoreBase.__init__(self) super(SplitMongoModuleStore, self).__init__(**kwargs)
if mongo_options is None: if mongo_options is None:
mongo_options = {} mongo_options = {}
...@@ -93,6 +96,11 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -93,6 +96,11 @@ class SplitMongoModuleStore(ModuleStoreBase):
self.error_tracker = error_tracker self.error_tracker = error_tracker
self.render_template = render_template self.render_template = render_template
# TODO: Don't have a runtime just to generate the appropriate mixin classes (cpennington)
# This is only used by _partition_fields_by_scope, which is only needed because
# the split mongo store is used for item creation as well as item persistence
self.mixologist = Mixologist(self.xblock_mixins)
def cache_items(self, system, base_usage_ids, depth=0, lazy=True): def cache_items(self, system, base_usage_ids, depth=0, lazy=True):
''' '''
Handles caching of items once inheritance and any other one time Handles caching of items once inheritance and any other one time
...@@ -144,13 +152,15 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -144,13 +152,15 @@ class SplitMongoModuleStore(ModuleStoreBase):
system = self._get_cache(course_entry['_id']) system = self._get_cache(course_entry['_id'])
if system is None: if system is None:
system = CachingDescriptorSystem( system = CachingDescriptorSystem(
self, modulestore=self,
course_entry, course_entry=course_entry,
{}, module_data={},
lazy, lazy=lazy,
self.default_class, default_class=self.default_class,
self.error_tracker, error_tracker=self.error_tracker,
self.render_template render_template=self.render_template,
resources_fs=None,
mixins=self.xblock_mixins
) )
self._add_cache(course_entry['_id'], system) self._add_cache(course_entry['_id'], system)
self.cache_items(system, usage_ids, depth, lazy) self.cache_items(system, usage_ids, depth, lazy)
...@@ -943,12 +953,12 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -943,12 +953,12 @@ class SplitMongoModuleStore(ModuleStoreBase):
xblock.definition_locator, is_updated = self.update_definition_from_data( xblock.definition_locator, is_updated = self.update_definition_from_data(
xblock.definition_locator, new_def_data, user_id) xblock.definition_locator, new_def_data, user_id)
if xblock.location.usage_id is None: if isinstance(xblock.scope_ids.usage_id.usage_id, LocalId):
# generate an id # generate an id
is_new = True is_new = True
is_updated = True is_updated = True
usage_id = self._generate_usage_id(structure_blocks, xblock.category) usage_id = self._generate_usage_id(structure_blocks, xblock.category)
xblock.location.usage_id = usage_id xblock.scope_ids.usage_id.usage_id = usage_id
else: else:
is_new = False is_new = False
usage_id = xblock.location.usage_id usage_id = xblock.location.usage_id
...@@ -960,9 +970,10 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -960,9 +970,10 @@ class SplitMongoModuleStore(ModuleStoreBase):
updated_blocks = [] updated_blocks = []
if xblock.has_children: if xblock.has_children:
for child in xblock.children: for child in xblock.children:
if isinstance(child, XModuleDescriptor): if isinstance(child.usage_id, LocalId):
updated_blocks += self._persist_subdag(child, user_id, structure_blocks) child_block = xblock.system.get_block(child)
children.append(child.location.usage_id) updated_blocks += self._persist_subdag(child_block, user_id, structure_blocks)
children.append(child_block.location.usage_id)
else: else:
children.append(child) children.append(child)
...@@ -1118,11 +1129,11 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1118,11 +1129,11 @@ class SplitMongoModuleStore(ModuleStoreBase):
""" """
return {} return {}
def inherit_settings(self, block_map, block, inheriting_settings=None): def inherit_settings(self, block_map, block_json, inheriting_settings=None):
""" """
Updates block with any inheritable setting set by an ancestor and recurses to children. Updates block_json with any inheritable setting set by an ancestor and recurses to children.
""" """
if block is None: if block_json is None:
return return
if inheriting_settings is None: if inheriting_settings is None:
...@@ -1132,14 +1143,14 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1132,14 +1143,14 @@ class SplitMongoModuleStore(ModuleStoreBase):
# NOTE: this should show the values which all fields would have if inherited: i.e., # NOTE: this should show the values which all fields would have if inherited: i.e.,
# not set to the locally defined value but to value set by nearest ancestor who sets it # not set to the locally defined value but to value set by nearest ancestor who sets it
# ALSO NOTE: no xblock should ever define a _inherited_settings field as it will collide w/ this logic. # ALSO NOTE: no xblock should ever define a _inherited_settings field as it will collide w/ this logic.
block.setdefault('_inherited_settings', {}).update(inheriting_settings) block_json.setdefault('_inherited_settings', {}).update(inheriting_settings)
# update the inheriting w/ what should pass to children # update the inheriting w/ what should pass to children
inheriting_settings = block['_inherited_settings'].copy() inheriting_settings = block_json['_inherited_settings'].copy()
block_fields = block['fields'] block_fields = block_json['fields']
for field in inheritance.INHERITABLE_METADATA: for field_name in inheritance.InheritanceMixin.fields:
if field in block_fields: if field_name in block_fields:
inheriting_settings[field] = block_fields[field] inheriting_settings[field_name] = block_fields[field_name]
for child in block_fields.get('children', []): for child in block_fields.get('children', []):
self.inherit_settings(block_map, block_map[child], inheriting_settings) self.inherit_settings(block_map, block_map[child], inheriting_settings)
...@@ -1308,7 +1319,7 @@ class SplitMongoModuleStore(ModuleStoreBase): ...@@ -1308,7 +1319,7 @@ class SplitMongoModuleStore(ModuleStoreBase):
""" """
if fields is None: if fields is None:
return {} return {}
cls = XModuleDescriptor.load_class(category) cls = self.mixologist.mix(XModuleDescriptor.load_class(category))
result = collections.defaultdict(dict) result = collections.defaultdict(dict)
for field_name, value in fields.iteritems(): for field_name, value in fields.iteritems():
field = getattr(cls, field_name) field = getattr(cls, field_name)
......
import copy import copy
from xblock.core import Scope from xblock.fields import Scope
from collections import namedtuple from collections import namedtuple
from xblock.runtime import KeyValueStore, InvalidScopeError from xblock.runtime import KeyValueStore
from xblock.exceptions import InvalidScopeError
from .definition_lazy_loader import DefinitionLazyLoader from .definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
# id is a BlockUsageLocator, def_id is the definition's guid # id is a BlockUsageLocator, def_id is the definition's guid
SplitMongoKVSid = namedtuple('SplitMongoKVSid', 'id, def_id') SplitMongoKVSid = namedtuple('SplitMongoKVSid', 'id, def_id')
PROVENANCE_LOCAL = 'local' class SplitMongoKVS(InheritanceKeyValueStore):
PROVENANCE_DEFAULT = 'default'
PROVENANCE_INHERITED = 'inherited'
class SplitMongoKVS(KeyValueStore):
""" """
A KeyValueStore that maps keyed data access to one of the 3 data areas A KeyValueStore that maps keyed data access to one of the 3 data areas
known to the MongoModuleStore (data, children, and metadata) known to the MongoModuleStore (data, children, and metadata)
""" """
def __init__(self, definition, fields, _inherited_settings, location, category): def __init__(self, definition, fields, inherited_settings):
""" """
:param definition: either a lazyloader or definition id for the definition :param definition: either a lazyloader or definition id for the definition
:param fields: a dictionary of the locally set fields :param fields: a dictionary of the locally set fields
:param _inherited_settings: the value of each inheritable field from above this. :param inherited_settings: the json value of each inheritable field from above this.
Note, local fields may override and disagree w/ this b/c this says what the value Note, local fields may override and disagree w/ this b/c this says what the value
should be if the field is undefined. should be if the field is undefined.
""" """
# ensure kvs's don't share objects w/ others so that changes can't appear in separate ones super(SplitMongoKVS, self).__init__(copy.copy(fields), inherited_settings)
# the particular use case was that changes to kvs's were polluting caches. My thinking was
# that kvs's should be independent thus responsible for the isolation.
self._definition = definition # either a DefinitionLazyLoader or the db id of the definition. self._definition = definition # either a DefinitionLazyLoader or the db id of the definition.
# if the db id, then the definition is presumed to be loaded into _fields # if the db id, then the definition is presumed to be loaded into _fields
self._fields = copy.copy(fields)
self._inherited_settings = _inherited_settings
self._location = location
self._category = category
def get(self, key): def get(self, key):
# simplest case, field is directly set # simplest case, field is directly set
...@@ -50,18 +43,10 @@ class SplitMongoKVS(KeyValueStore): ...@@ -50,18 +43,10 @@ class SplitMongoKVS(KeyValueStore):
# didn't find children in _fields; so, see if there's a default # didn't find children in _fields; so, see if there's a default
raise KeyError() raise KeyError()
elif key.scope == Scope.settings: elif key.scope == Scope.settings:
# didn't find in _fields; so, get from inheritance since not locally set # get default which may be the inherited value
if key.field_name in self._inherited_settings: raise KeyError()
return self._inherited_settings[key.field_name]
else:
# or get default
raise KeyError()
elif key.scope == Scope.content: elif key.scope == Scope.content:
if key.field_name == 'location': if isinstance(self._definition, DefinitionLazyLoader):
return self._location
elif key.field_name == 'category':
return self._category
elif isinstance(self._definition, DefinitionLazyLoader):
self._load_definition() self._load_definition()
if key.field_name in self._fields: if key.field_name in self._fields:
return self._fields[key.field_name] return self._fields[key.field_name]
...@@ -75,14 +60,7 @@ class SplitMongoKVS(KeyValueStore): ...@@ -75,14 +60,7 @@ class SplitMongoKVS(KeyValueStore):
if key.scope not in [Scope.children, Scope.settings, Scope.content]: if key.scope not in [Scope.children, Scope.settings, Scope.content]:
raise InvalidScopeError(key.scope) raise InvalidScopeError(key.scope)
if key.scope == Scope.content: if key.scope == Scope.content:
if key.field_name == 'location': self._load_definition()
self._location = value # is changing this legal?
return
elif key.field_name == 'category':
# TODO should this raise an exception? category is not changeable.
return
else:
self._load_definition()
# set the field # set the field
self._fields[key.field_name] = value self._fields[key.field_name] = value
...@@ -99,13 +77,7 @@ class SplitMongoKVS(KeyValueStore): ...@@ -99,13 +77,7 @@ class SplitMongoKVS(KeyValueStore):
if key.scope not in [Scope.children, Scope.settings, Scope.content]: if key.scope not in [Scope.children, Scope.settings, Scope.content]:
raise InvalidScopeError(key.scope) raise InvalidScopeError(key.scope)
if key.scope == Scope.content: if key.scope == Scope.content:
if key.field_name == 'location': self._load_definition()
return # noop
elif key.field_name == 'category':
# TODO should this raise an exception? category is not deleteable.
return # noop
else:
self._load_definition()
# delete the field value # delete the field value
if key.field_name in self._fields: if key.field_name in self._fields:
...@@ -123,53 +95,14 @@ class SplitMongoKVS(KeyValueStore): ...@@ -123,53 +95,14 @@ class SplitMongoKVS(KeyValueStore):
""" """
# handle any special cases # handle any special cases
if key.scope == Scope.content: if key.scope == Scope.content:
if key.field_name == 'location': self._load_definition()
return True
elif key.field_name == 'category':
return self._category is not None
else:
self._load_definition()
elif key.scope == Scope.parent: elif key.scope == Scope.parent:
return True return True
# it's not clear whether inherited values should return True. Right now they don't # it's not clear whether inherited values should return True. Right now they don't
# if someone changes it so that they do, then change any tests of field.name in xx._model_data # if someone changes it so that they do, then change any tests of field.name in xx._field_data
return key.field_name in self._fields return key.field_name in self._fields
# would like to just take a key, but there's a bunch of magic in DbModel for constructing the key via
# a private method
def field_value_provenance(self, key_scope, key_name):
"""
Where the field value comes from: one of [PROVENANCE_LOCAL, PROVENANCE_DEFAULT, PROVENANCE_INHERITED].
"""
# handle any special cases
if key_scope == Scope.content:
if key_name == 'location':
return PROVENANCE_LOCAL
elif key_name == 'category':
return PROVENANCE_LOCAL
else:
self._load_definition()
if key_name in self._fields:
return PROVENANCE_LOCAL
else:
return PROVENANCE_DEFAULT
elif key_scope == Scope.parent:
return PROVENANCE_DEFAULT
# catch the locally set state
elif key_name in self._fields:
return PROVENANCE_LOCAL
elif key_scope == Scope.settings and key_name in self._inherited_settings:
return PROVENANCE_INHERITED
else:
return PROVENANCE_DEFAULT
def get_inherited_settings(self):
"""
Get the settings set by the ancestors (which locally set fields may override or not)
"""
return self._inherited_settings
def _load_definition(self): def _load_definition(self):
""" """
Update fields w/ the lazily loaded definitions Update fields w/ the lazily loaded definitions
......
...@@ -110,12 +110,27 @@ def _clone_modules(modulestore, modules, source_location, dest_location): ...@@ -110,12 +110,27 @@ def _clone_modules(modulestore, modules, source_location, dest_location):
original_loc = Location(module.location) original_loc = Location(module.location)
if original_loc.category != 'course': if original_loc.category != 'course':
module.location = module.location._replace( new_location = module.location._replace(
tag=dest_location.tag, org=dest_location.org, course=dest_location.course) tag=dest_location.tag,
org=dest_location.org,
course=dest_location.course
)
module.scope_ids = module.scope_ids._replace(
def_id=new_location,
usage_id=new_location
)
else: else:
# on the course module we also have to update the module name # on the course module we also have to update the module name
module.location = module.location._replace( new_location = module.location._replace(
tag=dest_location.tag, org=dest_location.org, course=dest_location.course, name=dest_location.name) tag=dest_location.tag,
org=dest_location.org,
course=dest_location.course,
name=dest_location.name
)
module.scope_ids = module.scope_ids._replace(
def_id=new_location,
usage_id=new_location
)
print "Cloning module {0} to {1}....".format(original_loc, module.location) print "Cloning module {0} to {1}....".format(original_loc, module.location)
......
...@@ -6,8 +6,6 @@ from pytz import UTC ...@@ -6,8 +6,6 @@ from pytz import UTC
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import editable_modulestore from xmodule.modulestore.django import editable_modulestore
from xmodule.course_module import CourseDescriptor
from xblock.core import Scope
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
...@@ -35,7 +33,7 @@ class XModuleCourseFactory(Factory): ...@@ -35,7 +33,7 @@ class XModuleCourseFactory(Factory):
if display_name is not None: if display_name is not None:
new_course.display_name = display_name new_course.display_name = display_name
new_course.lms.start = datetime.datetime.now(UTC).replace(microsecond=0) new_course.start = datetime.datetime.now(UTC).replace(microsecond=0)
# The rest of kwargs become attributes on the course: # The rest of kwargs become attributes on the course:
for k, v in kwargs.iteritems(): for k, v in kwargs.iteritems():
......
...@@ -6,8 +6,9 @@ from nose.tools import assert_equals, assert_raises, \ ...@@ -6,8 +6,9 @@ from nose.tools import assert_equals, assert_raises, \
import pymongo import pymongo
from uuid import uuid4 from uuid import uuid4
from xblock.core import Scope from xblock.fields import Scope
from xblock.runtime import KeyValueStore, InvalidScopeError from xblock.runtime import KeyValueStore
from xblock.exceptions import InvalidScopeError
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -181,7 +182,7 @@ class TestMongoKeyValueStore(object): ...@@ -181,7 +182,7 @@ class TestMongoKeyValueStore(object):
self.location = Location('i4x://org/course/category/name@version') self.location = Location('i4x://org/course/category/name@version')
self.children = ['i4x://org/course/child/a', 'i4x://org/course/child/b'] self.children = ['i4x://org/course/child/a', 'i4x://org/course/child/b']
self.metadata = {'meta': 'meta_val'} self.metadata = {'meta': 'meta_val'}
self.kvs = MongoKeyValueStore(self.data, self.children, self.metadata, self.location, 'category') self.kvs = MongoKeyValueStore(self.data, self.children, self.metadata)
def _check_read(self, key, expected_value): def _check_read(self, key, expected_value):
""" """
...@@ -192,7 +193,6 @@ class TestMongoKeyValueStore(object): ...@@ -192,7 +193,6 @@ class TestMongoKeyValueStore(object):
def test_read(self): def test_read(self):
assert_equals(self.data['foo'], self.kvs.get(KeyValueStore.Key(Scope.content, None, None, 'foo'))) assert_equals(self.data['foo'], self.kvs.get(KeyValueStore.Key(Scope.content, None, None, 'foo')))
assert_equals(self.location, self.kvs.get(KeyValueStore.Key(Scope.content, None, None, 'location')))
assert_equals(self.children, self.kvs.get(KeyValueStore.Key(Scope.children, None, None, 'children'))) assert_equals(self.children, self.kvs.get(KeyValueStore.Key(Scope.children, None, None, 'children')))
assert_equals(self.metadata['meta'], self.kvs.get(KeyValueStore.Key(Scope.settings, None, None, 'meta'))) assert_equals(self.metadata['meta'], self.kvs.get(KeyValueStore.Key(Scope.settings, None, None, 'meta')))
assert_equals(None, self.kvs.get(KeyValueStore.Key(Scope.parent, None, None, 'parent'))) assert_equals(None, self.kvs.get(KeyValueStore.Key(Scope.parent, None, None, 'parent')))
...@@ -214,7 +214,6 @@ class TestMongoKeyValueStore(object): ...@@ -214,7 +214,6 @@ class TestMongoKeyValueStore(object):
def test_write(self): def test_write(self):
yield (self._check_write, KeyValueStore.Key(Scope.content, None, None, 'foo'), 'new_data') yield (self._check_write, KeyValueStore.Key(Scope.content, None, None, 'foo'), 'new_data')
yield (self._check_write, KeyValueStore.Key(Scope.content, None, None, 'location'), Location('i4x://org/course/category/name@new_version'))
yield (self._check_write, KeyValueStore.Key(Scope.children, None, None, 'children'), []) yield (self._check_write, KeyValueStore.Key(Scope.children, None, None, 'children'), [])
yield (self._check_write, KeyValueStore.Key(Scope.settings, None, None, 'meta'), 'new_settings') yield (self._check_write, KeyValueStore.Key(Scope.settings, None, None, 'meta'), 'new_settings')
...@@ -240,7 +239,6 @@ class TestMongoKeyValueStore(object): ...@@ -240,7 +239,6 @@ class TestMongoKeyValueStore(object):
def test_delete(self): def test_delete(self):
yield (self._check_delete_key_error, KeyValueStore.Key(Scope.content, None, None, 'foo')) yield (self._check_delete_key_error, KeyValueStore.Key(Scope.content, None, None, 'foo'))
yield (self._check_delete_default, KeyValueStore.Key(Scope.content, None, None, 'location'), Location(None))
yield (self._check_delete_default, KeyValueStore.Key(Scope.children, None, None, 'children'), []) yield (self._check_delete_default, KeyValueStore.Key(Scope.children, None, None, 'children'), [])
yield (self._check_delete_key_error, KeyValueStore.Key(Scope.settings, None, None, 'meta')) yield (self._check_delete_key_error, KeyValueStore.Key(Scope.settings, None, None, 'meta'))
......
...@@ -9,10 +9,11 @@ import unittest ...@@ -9,10 +9,11 @@ import unittest
import uuid import uuid
from importlib import import_module from importlib import import_module
from xblock.core import Scope from xblock.fields import Scope
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import InsufficientSpecificationError, ItemNotFoundError, VersionConflictError from xmodule.modulestore.exceptions import InsufficientSpecificationError, ItemNotFoundError, VersionConflictError
from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator, VersionTree, DescriptionLocator from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator, VersionTree, DescriptionLocator
from xmodule.modulestore.inheritance import InheritanceMixin
from pytz import UTC from pytz import UTC
from path import path from path import path
import re import re
...@@ -31,6 +32,7 @@ class SplitModuleTest(unittest.TestCase): ...@@ -31,6 +32,7 @@ class SplitModuleTest(unittest.TestCase):
'db': 'test_xmodule', 'db': 'test_xmodule',
'collection': 'modulestore{0}'.format(uuid.uuid4().hex), 'collection': 'modulestore{0}'.format(uuid.uuid4().hex),
'fs_root': '', 'fs_root': '',
'xblock_mixins': (InheritanceMixin,)
} }
MODULESTORE = { MODULESTORE = {
...@@ -187,7 +189,7 @@ class SplitModuleCourseTests(SplitModuleTest): ...@@ -187,7 +189,7 @@ class SplitModuleCourseTests(SplitModuleTest):
self.assertEqual(course.category, 'course') self.assertEqual(course.category, 'course')
self.assertEqual(len(course.tabs), 6) self.assertEqual(len(course.tabs), 6)
self.assertEqual(course.display_name, "The Ancient Greek Hero") self.assertEqual(course.display_name, "The Ancient Greek Hero")
self.assertEqual(course.lms.graceperiod, datetime.timedelta(hours=2)) self.assertEqual(course.graceperiod, datetime.timedelta(hours=2))
self.assertIsNone(course.advertised_start) self.assertIsNone(course.advertised_start)
self.assertEqual(len(course.children), 0) self.assertEqual(len(course.children), 0)
self.assertEqual(course.definition_locator.definition_id, "head12345_11") self.assertEqual(course.definition_locator.definition_id, "head12345_11")
...@@ -893,7 +895,7 @@ class TestCourseCreation(SplitModuleTest): ...@@ -893,7 +895,7 @@ class TestCourseCreation(SplitModuleTest):
original = modulestore().get_course(original_locator) original = modulestore().get_course(original_locator)
original_index = modulestore().get_course_index_info(original_locator) original_index = modulestore().get_course_index_info(original_locator)
fields = {} fields = {}
for field in original.fields: for field in original.fields.values():
if field.scope == Scope.content and field.name != 'location': if field.scope == Scope.content and field.name != 'location':
fields[field.name] = getattr(original, field.name) fields[field.name] = getattr(original, field.name)
elif field.scope == Scope.settings: elif field.scope == Scope.settings:
......
...@@ -20,8 +20,11 @@ from xmodule.mako_module import MakoDescriptorSystem ...@@ -20,8 +20,11 @@ from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
from xmodule.html_module import HtmlDescriptor from xmodule.html_module import HtmlDescriptor
from xblock.fields import ScopeIds
from xblock.field_data import DictFieldData
from . import ModuleStoreBase, Location, XML_MODULESTORE_TYPE from . import ModuleStoreBase, Location, XML_MODULESTORE_TYPE
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
from .inheritance import compute_inherited_metadata from .inheritance import compute_inherited_metadata
...@@ -44,7 +47,7 @@ def clean_out_mako_templating(xml_string): ...@@ -44,7 +47,7 @@ def clean_out_mako_templating(xml_string):
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
def __init__(self, xmlstore, course_id, course_dir, def __init__(self, xmlstore, course_id, course_dir,
policy, error_tracker, parent_tracker, error_tracker, parent_tracker,
load_error_modules=True, **kwargs): load_error_modules=True, **kwargs):
""" """
A class that handles loading from xml. Does some munging to ensure that A class that handles loading from xml. Does some munging to ensure that
...@@ -206,11 +209,14 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -206,11 +209,14 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# policy to be loaded. For now, just add the course_id here... # policy to be loaded. For now, just add the course_id here...
load_item = lambda location: xmlstore.get_instance(course_id, location) load_item = lambda location: xmlstore.get_instance(course_id, location)
resources_fs = OSFS(xmlstore.data_dir / course_dir) resources_fs = OSFS(xmlstore.data_dir / course_dir)
super(ImportSystem, self).__init__(
MakoDescriptorSystem.__init__(self, load_item, resources_fs, load_item=load_item,
error_tracker, render_template, **kwargs) resources_fs=resources_fs,
XMLParsingSystem.__init__(self, load_item, resources_fs, render_template=render_template,
error_tracker, process_xml, policy, **kwargs) error_tracker=error_tracker,
process_xml=process_xml,
**kwargs
)
class ParentTracker(object): class ParentTracker(object):
...@@ -412,13 +418,14 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -412,13 +418,14 @@ class XMLModuleStore(ModuleStoreBase):
course_id = CourseDescriptor.make_id(org, course, url_name) course_id = CourseDescriptor.make_id(org, course, url_name)
system = ImportSystem( system = ImportSystem(
self, xmlstore=self,
course_id, course_id=course_id,
course_dir, course_dir=course_dir,
policy, error_tracker=tracker,
tracker, parent_tracker=self.parent_trackers[course_id],
self.parent_trackers[course_id], load_error_modules=self.load_error_modules,
self.load_error_modules, policy=policy,
mixins=self.xblock_mixins,
) )
course_descriptor = system.process_xml(etree.tostring(course_data, encoding='unicode')) course_descriptor = system.process_xml(etree.tostring(course_data, encoding='unicode'))
...@@ -467,9 +474,13 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -467,9 +474,13 @@ class XMLModuleStore(ModuleStoreBase):
# tabs are referenced in policy.json through a 'slug' which is just the filename without the .html suffix # 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] slug = os.path.splitext(os.path.basename(filepath))[0]
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug) loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
module = HtmlDescriptor( module = system.construct_xblock_from_class(
system, HtmlDescriptor,
{'data': html, 'location': loc, 'category': category} DictFieldData({'data': html, 'location': loc, 'category': category}),
# We're loading a descriptor, so student_id is meaningless
# We also don't have separate notions of definition and usage ids yet,
# so we use the location for both
ScopeIds(None, category, loc, loc),
) )
# VS[compat]: # VS[compat]:
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them) # Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
......
...@@ -3,7 +3,7 @@ import os ...@@ -3,7 +3,7 @@ import os
import mimetypes import mimetypes
from path import path from path import path
from xblock.core import Scope from xblock.fields import Scope
from .xml import XMLModuleStore, ImportSystem, ParentTracker from .xml import XMLModuleStore, ImportSystem, ParentTracker
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -25,7 +25,7 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ ...@@ -25,7 +25,7 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
verbose = True verbose = True
for dirname, dirnames, filenames in os.walk(static_dir): for dirname, _, filenames in os.walk(static_dir):
for filename in filenames: for filename in filenames:
try: try:
...@@ -91,7 +91,8 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -91,7 +91,8 @@ def import_from_xml(store, data_dir, course_dirs=None,
data_dir, data_dir,
default_class=default_class, default_class=default_class,
course_dirs=course_dirs, course_dirs=course_dirs,
load_error_modules=load_error_modules load_error_modules=load_error_modules,
xblock_mixins=store.xblock_mixins,
) )
# NOTE: the XmlModuleStore does not implement get_items() which would be a preferable means # NOTE: the XmlModuleStore does not implement get_items() which would be a preferable means
...@@ -120,7 +121,7 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -120,7 +121,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
# Quick scan to get course module as we need some info from there. Also we need to make sure that the # Quick scan to get course module as we need some info from there. Also we need to make sure that the
# course module is committed first into the store # course module is committed first into the store
for module in xml_module_store.modules[course_id].itervalues(): for module in xml_module_store.modules[course_id].itervalues():
if module.category == 'course': if module.scope_ids.block_type == 'course':
course_data_path = path(data_dir) / module.data_dir course_data_path = path(data_dir) / module.data_dir
course_location = module.location course_location = module.location
...@@ -129,9 +130,9 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -129,9 +130,9 @@ def import_from_xml(store, data_dir, course_dirs=None,
module = remap_namespace(module, target_location_namespace) module = remap_namespace(module, target_location_namespace)
if not do_import_static: if not do_import_static:
module.lms.static_asset_path = module.data_dir # for old-style xblock where this was actually linked to kvs module.static_asset_path = module.data_dir # for old-style xblock where this was actually linked to kvs
module._model_data['static_asset_path'] = module.data_dir module.save()
log.debug('course static_asset_path={0}'.format(module.lms.static_asset_path)) log.debug('course static_asset_path={0}'.format(module.static_asset_path))
log.debug('course data_dir={0}'.format(module.data_dir)) log.debug('course data_dir={0}'.format(module.data_dir))
...@@ -177,7 +178,7 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -177,7 +178,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
# finally loop through all the modules # finally loop through all the modules
for module in xml_module_store.modules[course_id].itervalues(): for module in xml_module_store.modules[course_id].itervalues():
if module.category == 'course': if module.scope_ids.block_type == 'course':
# we've already saved the course module up at the top of the loop # we've already saved the course module up at the top of the loop
# so just skip over it in the inner loop # so just skip over it in the inner loop
continue continue
...@@ -195,9 +196,15 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -195,9 +196,15 @@ def import_from_xml(store, data_dir, course_dirs=None,
# now import any 'draft' items # now import any 'draft' items
if draft_store is not None: if draft_store is not None:
import_course_draft(xml_module_store, store, draft_store, course_data_path, import_course_draft(
static_content_store, course_location, target_location_namespace if target_location_namespace xml_module_store,
else course_location) store,
draft_store,
course_data_path,
static_content_store,
course_location,
target_location_namespace if target_location_namespace else course_location
)
finally: finally:
# turn back on all write signalling # turn back on all write signalling
...@@ -217,13 +224,13 @@ def import_module(module, store, course_data_path, static_content_store, ...@@ -217,13 +224,13 @@ def import_module(module, store, course_data_path, static_content_store,
logging.debug('processing import of module {0}...'.format(module.location.url())) logging.debug('processing import of module {0}...'.format(module.location.url()))
content = {} content = {}
for field in module.fields: for field in module.fields.values():
if field.scope != Scope.content: if field.scope != Scope.content:
continue continue
try: try:
content[field.name] = module._model_data[field.name] content[field.name] = module._field_data.get(module, field.name)
except KeyError: except KeyError:
# Ignore any missing keys in _model_data # Ignore any missing keys in _field_data
pass pass
module_data = {} module_data = {}
...@@ -274,13 +281,13 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path, ...@@ -274,13 +281,13 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path,
# create a new 'System' object which will manage the importing # create a new 'System' object which will manage the importing
errorlog = make_error_tracker() errorlog = make_error_tracker()
system = ImportSystem( system = ImportSystem(
xml_module_store, xmlstore=xml_module_store,
target_location_namespace.course_id, course_id=target_location_namespace.course_id,
draft_dir, course_dir=draft_dir,
{}, policy={},
errorlog.tracker, error_tracker=errorlog.tracker,
ParentTracker(), parent_tracker=ParentTracker(),
None, load_error_modules=False,
) )
# now walk the /vertical directory where each file in there will be a draft copy of the Vertical # now walk the /vertical directory where each file in there will be a draft copy of the Vertical
...@@ -368,15 +375,30 @@ def remap_namespace(module, target_location_namespace): ...@@ -368,15 +375,30 @@ def remap_namespace(module, target_location_namespace):
# This looks a bit wonky as we need to also change the 'name' of the imported course to be what # This looks a bit wonky as we need to also change the 'name' of the imported course to be what
# the caller passed in # the caller passed in
if module.location.category != 'course': if module.location.category != 'course':
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, new_location = module.location._replace(
course=target_location_namespace.course) tag=target_location_namespace.tag,
org=target_location_namespace.org,
course=target_location_namespace.course
)
module.scope_ids = module.scope_ids._replace(
def_id=new_location,
usage_id=new_location
)
else: else:
original_location = module.location original_location = module.location
# #
# module is a course module # module is a course module
# #
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, new_location = module.location._replace(
course=target_location_namespace.course, name=target_location_namespace.name) tag=target_location_namespace.tag,
org=target_location_namespace.org,
course=target_location_namespace.course,
name=target_location_namespace.name
)
module.scope_ids = module.scope_ids._replace(
def_id=new_location,
usage_id=new_location
)
# #
# There is more re-namespacing work we have to do when importing course modules # There is more re-namespacing work we have to do when importing course modules
# #
...@@ -401,8 +423,11 @@ def remap_namespace(module, target_location_namespace): ...@@ -401,8 +423,11 @@ def remap_namespace(module, target_location_namespace):
new_locs = [] new_locs = []
for child in children_locs: for child in children_locs:
child_loc = Location(child) child_loc = Location(child)
new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org, new_child_loc = child_loc._replace(
course=target_location_namespace.course) tag=target_location_namespace.tag,
org=target_location_namespace.org,
course=target_location_namespace.course
)
new_locs.append(new_child_loc.url()) new_locs.append(new_child_loc.url())
...@@ -501,10 +526,10 @@ def validate_course_policy(module_store, course_id): ...@@ -501,10 +526,10 @@ def validate_course_policy(module_store, course_id):
warn_cnt = 0 warn_cnt = 0
for module in module_store.modules[course_id].itervalues(): for module in module_store.modules[course_id].itervalues():
if module.location.category == 'course': if module.location.category == 'course':
if not 'rerandomize' in module._model_data: if not module._field_data.has(module, 'rerandomize'):
warn_cnt += 1 warn_cnt += 1
print 'WARN: course policy does not specify value for "rerandomize" whose default is now "never". The behavior of your course may change.' print 'WARN: course policy does not specify value for "rerandomize" whose default is now "never". The behavior of your course may change.'
if not 'showanswer' in module._model_data: if not module._field_data.has(module, 'showanswer'):
warn_cnt += 1 warn_cnt += 1
print 'WARN: course policy does not specify value for "showanswer" whose default is now "finished". The behavior of your course may change.' print 'WARN: course policy does not specify value for "showanswer" whose default is now "finished". The behavior of your course may change.'
return warn_cnt return warn_cnt
......
...@@ -10,7 +10,7 @@ from .x_module import XModule ...@@ -10,7 +10,7 @@ from .x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from .timeinfo import TimeInfo from .timeinfo import TimeInfo
from xblock.core import Dict, String, Scope, Boolean, Float from xblock.fields import Dict, String, Scope, Boolean, Float
from xmodule.fields import Date, Timedelta from xmodule.fields import Date, Timedelta
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
...@@ -108,9 +108,9 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -108,9 +108,9 @@ class PeerGradingModule(PeerGradingFields, XModule):
log.error("Linked location {0} for peer grading module {1} does not exist".format( log.error("Linked location {0} for peer grading module {1} does not exist".format(
self.link_to_location, self.location)) self.link_to_location, self.location))
raise raise
due_date = self.linked_problem.lms.due due_date = self.linked_problem.due
if due_date: if due_date:
self.lms.due = due_date self.due = due_date
try: try:
self.timeinfo = TimeInfo(self.due, self.graceperiod) self.timeinfo = TimeInfo(self.due, self.graceperiod)
...@@ -532,8 +532,8 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -532,8 +532,8 @@ class PeerGradingModule(PeerGradingFields, XModule):
except Exception: except Exception:
continue continue
if descriptor: if descriptor:
problem['due'] = descriptor.lms.due problem['due'] = descriptor.due
grace_period = descriptor.lms.graceperiod grace_period = descriptor.graceperiod
try: try:
problem_timeinfo = TimeInfo(problem['due'], grace_period) problem_timeinfo = TimeInfo(problem['due'], grace_period)
except Exception: except Exception:
......
import pkg_resources
import logging
log = logging.getLogger(__name__)
class PluginNotFoundError(Exception):
pass
class Plugin(object):
"""
Base class for a system that uses entry_points to load plugins.
Implementing classes are expected to have the following attributes:
entry_point: The name of the entry point to load plugins from
"""
_plugin_cache = None
@classmethod
def load_class(cls, identifier, default=None):
"""
Loads a single class instance specified by identifier. If identifier
specifies more than a single class, then logs a warning and returns the
first class identified.
If default is not None, will return default if no entry_point matching
identifier is found. Otherwise, will raise a ModuleMissingError
"""
if cls._plugin_cache is None:
cls._plugin_cache = {}
if identifier not in cls._plugin_cache:
identifier = identifier.lower()
classes = list(pkg_resources.iter_entry_points(
cls.entry_point, name=identifier))
if len(classes) > 1:
log.warning("Found multiple classes for {entry_point} with "
"identifier {id}: {classes}. "
"Returning the first one.".format(
entry_point=cls.entry_point,
id=identifier,
classes=", ".join(
class_.module_name for class_ in classes)))
if len(classes) == 0:
if default is not None:
return default
raise PluginNotFoundError(identifier)
cls._plugin_cache[identifier] = classes[0].load()
return cls._plugin_cache[identifier]
@classmethod
def load_classes(cls):
"""
Returns a list of containing the identifiers and their corresponding classes for all
of the available instances of this plugin
"""
return [(class_.name, class_.load())
for class_
in pkg_resources.iter_entry_points(cls.entry_point)]
...@@ -19,7 +19,7 @@ from xmodule.x_module import XModule ...@@ -19,7 +19,7 @@ from xmodule.x_module import XModule
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
from xmodule.mako_module import MakoModuleDescriptor from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, String, Dict, Boolean, List from xblock.fields import Scope, String, Dict, Boolean, List
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -30,7 +30,7 @@ class PollFields(object): ...@@ -30,7 +30,7 @@ class PollFields(object):
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False) voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False)
poll_answer = String(help="Student answer", scope=Scope.user_state, default='') poll_answer = String(help="Student answer", scope=Scope.user_state, default='')
poll_answers = Dict(help="All possible answers for the poll fro other students", scope=Scope.content) poll_answers = Dict(help="All possible answers for the poll fro other students", scope=Scope.user_state_summary)
answers = List(help="Poll answers from xml", scope=Scope.content, default=[]) answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
question = String(help="Poll question", scope=Scope.content, default='') question = String(help="Poll question", scope=Scope.content, default='')
......
...@@ -6,7 +6,7 @@ from xmodule.seq_module import SequenceDescriptor ...@@ -6,7 +6,7 @@ from xmodule.seq_module import SequenceDescriptor
from lxml import etree from lxml import etree
from xblock.core import Scope, Integer from xblock.fields import Scope, Integer
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
......
...@@ -3,7 +3,7 @@ from xmodule.editing_module import XMLEditingDescriptor ...@@ -3,7 +3,7 @@ from xmodule.editing_module import XMLEditingDescriptor
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
import logging import logging
import sys import sys
from xblock.core import String, Scope from xblock.fields import String, Scope
from exceptions import SerializationError from exceptions import SerializationError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -8,7 +8,7 @@ from xmodule.xml_module import XmlDescriptor ...@@ -8,7 +8,7 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.progress import Progress from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xblock.core import Integer, Scope from xblock.fields import Integer, Scope
from pkg_resources import resource_string from pkg_resources import resource_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -40,8 +40,8 @@ class SequenceModule(SequenceFields, XModule): ...@@ -40,8 +40,8 @@ class SequenceModule(SequenceFields, XModule):
XModule.__init__(self, *args, **kwargs) XModule.__init__(self, *args, **kwargs)
# if position is specified in system, then use that instead # if position is specified in system, then use that instead
if self.system.get('position'): if getattr(self.system, 'position', None) is not None:
self.position = int(self.system.get('position')) self.position = int(self.system.position)
self.rendered = False self.rendered = False
......
...@@ -18,7 +18,10 @@ from mock import Mock ...@@ -18,7 +18,10 @@ from mock import Mock
from path import path from path import path
import calc import calc
from xmodule.x_module import ModuleSystem, XModuleDescriptor from xblock.field_data import DictFieldData
from xmodule.x_module import ModuleSystem, XModuleDescriptor, DescriptorSystem
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.mako_module import MakoDescriptorSystem
# Location of common test DATA directory # Location of common test DATA directory
...@@ -61,9 +64,22 @@ def get_test_system(): ...@@ -61,9 +64,22 @@ def get_test_system():
debug=True, debug=True,
xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10, 'construct_callback' : Mock(side_effect="/")}, xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10, 'construct_callback' : Mock(side_effect="/")},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
xblock_model_data=lambda descriptor: descriptor._model_data, xblock_field_data=lambda descriptor: descriptor._field_data,
anonymous_student_id='student', anonymous_student_id='student',
open_ended_grading_interface= open_ended_grading_interface open_ended_grading_interface=open_ended_grading_interface
)
def get_test_descriptor_system():
"""
Construct a test DescriptorSystem instance.
"""
return MakoDescriptorSystem(
load_item=Mock(),
resources_fs=Mock(),
error_tracker=Mock(),
render_template=lambda template, context: repr(context),
mixins=(InheritanceMixin,),
) )
...@@ -89,7 +105,7 @@ class PostData(object): ...@@ -89,7 +105,7 @@ class PostData(object):
class LogicTest(unittest.TestCase): class LogicTest(unittest.TestCase):
"""Base class for testing xmodule logic.""" """Base class for testing xmodule logic."""
descriptor_class = None descriptor_class = None
raw_model_data = {} raw_field_data = {}
def setUp(self): def setUp(self):
class EmptyClass: class EmptyClass:
...@@ -102,7 +118,8 @@ class LogicTest(unittest.TestCase): ...@@ -102,7 +118,8 @@ class LogicTest(unittest.TestCase):
self.xmodule_class = self.descriptor_class.module_class self.xmodule_class = self.descriptor_class.module_class
self.xmodule = self.xmodule_class( self.xmodule = self.xmodule_class(
self.system, self.descriptor, self.raw_model_data) self.descriptor, self.system, DictFieldData(self.raw_field_data), Mock()
)
def ajax_request(self, dispatch, data): def ajax_request(self, dispatch, data):
"""Call Xmodule.handle_ajax.""" """Call Xmodule.handle_ajax."""
......
...@@ -5,6 +5,8 @@ import unittest ...@@ -5,6 +5,8 @@ import unittest
from lxml import etree from lxml import etree
from mock import Mock from mock import Mock
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xmodule.annotatable_module import AnnotatableModule from xmodule.annotatable_module import AnnotatableModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -29,10 +31,15 @@ class AnnotatableModuleTestCase(unittest.TestCase): ...@@ -29,10 +31,15 @@ class AnnotatableModuleTestCase(unittest.TestCase):
</annotatable> </annotatable>
''' '''
descriptor = Mock() descriptor = Mock()
module_data = {'data': sample_xml, 'location': location} field_data = DictFieldData({'data': sample_xml})
def setUp(self): def setUp(self):
self.annotatable = AnnotatableModule(get_test_system(), self.descriptor, self.module_data) self.annotatable = AnnotatableModule(
self.descriptor,
get_test_system(),
self.field_data,
ScopeIds(None, None, None, None)
)
def test_annotation_data_attr(self): def test_annotation_data_attr(self):
el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>') el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
......
...@@ -18,6 +18,8 @@ from capa.responsetypes import (StudentInputError, LoncapaProblemError, ...@@ -18,6 +18,8 @@ from capa.responsetypes import (StudentInputError, LoncapaProblemError,
ResponseError) ResponseError)
from xmodule.capa_module import CapaModule, ComplexEncoder from xmodule.capa_module import CapaModule, ComplexEncoder
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from django.http import QueryDict from django.http import QueryDict
...@@ -95,34 +97,39 @@ class CapaFactory(object): ...@@ -95,34 +97,39 @@ class CapaFactory(object):
""" """
location = Location(["i4x", "edX", "capa_test", "problem", location = Location(["i4x", "edX", "capa_test", "problem",
"SampleProblem{0}".format(CapaFactory.next_num())]) "SampleProblem{0}".format(CapaFactory.next_num())])
model_data = {'data': CapaFactory.sample_problem_xml, 'location': location} field_data = {'data': CapaFactory.sample_problem_xml}
if graceperiod is not None: if graceperiod is not None:
model_data['graceperiod'] = graceperiod field_data['graceperiod'] = graceperiod
if due is not None: if due is not None:
model_data['due'] = due field_data['due'] = due
if max_attempts is not None: if max_attempts is not None:
model_data['max_attempts'] = max_attempts field_data['max_attempts'] = max_attempts
if showanswer is not None: if showanswer is not None:
model_data['showanswer'] = showanswer field_data['showanswer'] = showanswer
if force_save_button is not None: if force_save_button is not None:
model_data['force_save_button'] = force_save_button field_data['force_save_button'] = force_save_button
if rerandomize is not None: if rerandomize is not None:
model_data['rerandomize'] = rerandomize field_data['rerandomize'] = rerandomize
if done is not None: if done is not None:
model_data['done'] = done field_data['done'] = done
descriptor = Mock(weight="1") descriptor = Mock(weight="1")
if problem_state is not None: if problem_state is not None:
model_data.update(problem_state) field_data.update(problem_state)
if attempts is not None: if attempts is not None:
# converting to int here because I keep putting "0" and "1" in the tests # converting to int here because I keep putting "0" and "1" in the tests
# since everything else is a string. # since everything else is a string.
model_data['attempts'] = int(attempts) field_data['attempts'] = int(attempts)
system = get_test_system() system = get_test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
module = CapaModule(system, descriptor, model_data) module = CapaModule(
descriptor,
system,
DictFieldData(field_data),
ScopeIds(None, None, location, location),
)
if correct: if correct:
# TODO: probably better to actually set the internal state properly, but... # TODO: probably better to actually set the internal state properly, but...
......
...@@ -28,6 +28,9 @@ from xmodule.tests.test_util_open_ended import ( ...@@ -28,6 +28,9 @@ from xmodule.tests.test_util_open_ended import (
MOCK_INSTANCE_STATE, TEST_STATE_SA, TEST_STATE_AI, TEST_STATE_AI2, TEST_STATE_AI2_INVALID, MOCK_INSTANCE_STATE, TEST_STATE_SA, TEST_STATE_AI, TEST_STATE_AI2, TEST_STATE_AI2_INVALID,
TEST_STATE_SINGLE, TEST_STATE_PE_SINGLE TEST_STATE_SINGLE, TEST_STATE_PE_SINGLE
) )
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
import capa.xqueue_interface as xqueue_interface import capa.xqueue_interface as xqueue_interface
...@@ -418,13 +421,13 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -418,13 +421,13 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
test_system = get_test_system() test_system = get_test_system()
test_system.open_ended_grading_interface = None test_system.open_ended_grading_interface = None
combinedoe_container = CombinedOpenEndedModule( combinedoe_container = CombinedOpenEndedModule(
test_system, descriptor=descriptor,
descriptor, runtime=test_system,
model_data={ field_data=DictFieldData({
'data': full_definition, 'data': full_definition,
'weight': '1', 'weight': '1',
'location': location }),
} scope_ids=ScopeIds(None, None, None, None),
) )
def setUp(self): def setUp(self):
......
...@@ -6,6 +6,8 @@ import unittest ...@@ -6,6 +6,8 @@ import unittest
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from mock import Mock, patch from mock import Mock, patch
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xmodule.error_module import NonStaffErrorDescriptor from xmodule.error_module import NonStaffErrorDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
...@@ -87,8 +89,13 @@ class ConditionalFactory(object): ...@@ -87,8 +89,13 @@ class ConditionalFactory(object):
# construct conditional module: # construct conditional module:
cond_location = Location(["i4x", "edX", "conditional_test", "conditional", "SampleConditional"]) cond_location = Location(["i4x", "edX", "conditional_test", "conditional", "SampleConditional"])
model_data = {'data': '<conditional/>', 'location': cond_location} field_data = DictFieldData({'data': '<conditional/>', 'location': cond_location})
cond_module = ConditionalModule(system, cond_descriptor, model_data) cond_module = ConditionalModule(
cond_descriptor,
system,
field_data,
ScopeIds(None, None, cond_location, cond_location)
)
# return dict: # return dict:
return {'cond_module': cond_module, return {'cond_module': cond_module,
......
...@@ -29,12 +29,12 @@ class DummySystem(ImportSystem): ...@@ -29,12 +29,12 @@ class DummySystem(ImportSystem):
parent_tracker = Mock() parent_tracker = Mock()
super(DummySystem, self).__init__( super(DummySystem, self).__init__(
xmlstore, xmlstore=xmlstore,
course_id, course_id=course_id,
course_dir, course_dir=course_dir,
policy, policy=policy,
error_tracker, error_tracker=error_tracker,
parent_tracker, parent_tracker=parent_tracker,
load_error_modules=load_error_modules, load_error_modules=load_error_modules,
) )
......
...@@ -8,6 +8,7 @@ import copy ...@@ -8,6 +8,7 @@ import copy
from xmodule.crowdsource_hinter import CrowdsourceHinterModule from xmodule.crowdsource_hinter import CrowdsourceHinterModule
from xmodule.vertical_module import VerticalModule, VerticalDescriptor from xmodule.vertical_module import VerticalModule, VerticalDescriptor
from xblock.field_data import DictFieldData
from . import get_test_system from . import get_test_system
...@@ -60,12 +61,12 @@ class CHModuleFactory(object): ...@@ -60,12 +61,12 @@ class CHModuleFactory(object):
""" """
A factory method for making CHM's A factory method for making CHM's
""" """
model_data = {'data': CHModuleFactory.sample_problem_xml} field_data = {'data': CHModuleFactory.sample_problem_xml}
if hints is not None: if hints is not None:
model_data['hints'] = hints field_data['hints'] = hints
else: else:
model_data['hints'] = { field_data['hints'] = {
'24.0': {'0': ['Best hint', 40], '24.0': {'0': ['Best hint', 40],
'3': ['Another hint', 30], '3': ['Another hint', 30],
'4': ['A third hint', 20], '4': ['A third hint', 20],
...@@ -74,31 +75,31 @@ class CHModuleFactory(object): ...@@ -74,31 +75,31 @@ class CHModuleFactory(object):
} }
if mod_queue is not None: if mod_queue is not None:
model_data['mod_queue'] = mod_queue field_data['mod_queue'] = mod_queue
else: else:
model_data['mod_queue'] = { field_data['mod_queue'] = {
'24.0': {'2': ['A non-approved hint']}, '24.0': {'2': ['A non-approved hint']},
'26.0': {'5': ['Another non-approved hint']} '26.0': {'5': ['Another non-approved hint']}
} }
if previous_answers is not None: if previous_answers is not None:
model_data['previous_answers'] = previous_answers field_data['previous_answers'] = previous_answers
else: else:
model_data['previous_answers'] = [ field_data['previous_answers'] = [
['24.0', [0, 3, 4]], ['24.0', [0, 3, 4]],
['29.0', []] ['29.0', []]
] ]
if user_submissions is not None: if user_submissions is not None:
model_data['user_submissions'] = user_submissions field_data['user_submissions'] = user_submissions
else: else:
model_data['user_submissions'] = ['24.0', '29.0'] field_data['user_submissions'] = ['24.0', '29.0']
if user_voted is not None: if user_voted is not None:
model_data['user_voted'] = user_voted field_data['user_voted'] = user_voted
if moderate is not None: if moderate is not None:
model_data['moderate'] = moderate field_data['moderate'] = moderate
descriptor = Mock(weight='1') descriptor = Mock(weight='1')
# Make the descriptor have a capa problem child. # Make the descriptor have a capa problem child.
...@@ -138,8 +139,7 @@ class CHModuleFactory(object): ...@@ -138,8 +139,7 @@ class CHModuleFactory(object):
if descriptor.name == 'capa': if descriptor.name == 'capa':
return capa_module return capa_module
system.get_module = fake_get_module system.get_module = fake_get_module
module = CrowdsourceHinterModule(descriptor, system, DictFieldData(field_data), Mock())
module = CrowdsourceHinterModule(system, descriptor, model_data)
return module return module
...@@ -196,10 +196,10 @@ class VerticalWithModulesFactory(object): ...@@ -196,10 +196,10 @@ class VerticalWithModulesFactory(object):
@staticmethod @staticmethod
def create(): def create():
"""Make a vertical.""" """Make a vertical."""
model_data = {'data': VerticalWithModulesFactory.sample_problem_xml} field_data = {'data': VerticalWithModulesFactory.sample_problem_xml}
system = get_test_system() system = get_test_system()
descriptor = VerticalDescriptor.from_xml(VerticalWithModulesFactory.sample_problem_xml, system) descriptor = VerticalDescriptor.from_xml(VerticalWithModulesFactory.sample_problem_xml, system)
module = VerticalModule(system, descriptor, model_data) module = VerticalModule(system, descriptor, field_data)
return module return module
......
...@@ -6,8 +6,10 @@ import logging ...@@ -6,8 +6,10 @@ import logging
from mock import Mock from mock import Mock
from pkg_resources import resource_string from pkg_resources import resource_string
from xmodule.editing_module import TabsEditingDescriptor from xmodule.editing_module import TabsEditingDescriptor
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from .import get_test_system from xmodule.tests import get_test_descriptor_system
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -17,7 +19,7 @@ class TabsEditingDescriptorTestCase(unittest.TestCase): ...@@ -17,7 +19,7 @@ class TabsEditingDescriptorTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
super(TabsEditingDescriptorTestCase, self).setUp() super(TabsEditingDescriptorTestCase, self).setUp()
system = get_test_system() system = get_test_descriptor_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
self.tabs = [ self.tabs = [
{ {
...@@ -42,9 +44,11 @@ class TabsEditingDescriptorTestCase(unittest.TestCase): ...@@ -42,9 +44,11 @@ class TabsEditingDescriptorTestCase(unittest.TestCase):
] ]
TabsEditingDescriptor.tabs = self.tabs TabsEditingDescriptor.tabs = self.tabs
self.descriptor = TabsEditingDescriptor( self.descriptor = system.construct_xblock_from_class(
runtime=system, TabsEditingDescriptor,
model_data={}) field_data=DictFieldData({}),
scope_ids=ScopeIds(None, None, None, None),
)
def test_get_css(self): def test_get_css(self):
"""test get_css""" """test get_css"""
......
...@@ -39,7 +39,7 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules): ...@@ -39,7 +39,7 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = MagicMock([XModuleDescriptor], descriptor = MagicMock([XModuleDescriptor],
system=self.system, system=self.system,
location=self.location, location=self.location,
_model_data=self.valid_xml) _field_data=self.valid_xml)
error_descriptor = error_module.ErrorDescriptor.from_descriptor( error_descriptor = error_module.ErrorDescriptor.from_descriptor(
descriptor, self.error_msg) descriptor, self.error_msg)
...@@ -74,7 +74,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules): ...@@ -74,7 +74,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor = MagicMock([XModuleDescriptor], descriptor = MagicMock([XModuleDescriptor],
system=self.system, system=self.system,
location=self.location, location=self.location,
_model_data=self.valid_xml) _field_data=self.valid_xml)
error_descriptor = error_module.NonStaffErrorDescriptor.from_descriptor( error_descriptor = error_module.NonStaffErrorDescriptor.from_descriptor(
descriptor, self.error_msg) descriptor, self.error_msg)
......
...@@ -24,7 +24,8 @@ def strip_filenames(descriptor): ...@@ -24,7 +24,8 @@ def strip_filenames(descriptor):
Recursively strips 'filename' from all children's definitions. Recursively strips 'filename' from all children's definitions.
""" """
print("strip filename from {desc}".format(desc=descriptor.location.url())) print("strip filename from {desc}".format(desc=descriptor.location.url()))
descriptor._model_data.pop('filename', None) if descriptor._field_data.has(descriptor, 'filename'):
descriptor._field_data.delete(descriptor, 'filename')
if hasattr(descriptor, 'xml_attributes'): if hasattr(descriptor, 'xml_attributes'):
if 'filename' in descriptor.xml_attributes: if 'filename' in descriptor.xml_attributes:
......
...@@ -2,6 +2,7 @@ import unittest ...@@ -2,6 +2,7 @@ import unittest
from mock import Mock from mock import Mock
from xblock.field_data import DictFieldData
from xmodule.html_module import HtmlModule from xmodule.html_module import HtmlModule
from . import get_test_system from . import get_test_system
...@@ -11,9 +12,9 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase): ...@@ -11,9 +12,9 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
def test_substitution_works(self): def test_substitution_works(self):
sample_xml = '''%%USER_ID%%''' sample_xml = '''%%USER_ID%%'''
module_data = {'data': sample_xml} field_data = DictFieldData({'data': sample_xml})
module_system = get_test_system() module_system = get_test_system()
module = HtmlModule(module_system, self.descriptor, module_data) module = HtmlModule(self.descriptor, module_system, field_data, Mock())
self.assertEqual(module.get_html(), str(module_system.anonymous_student_id)) self.assertEqual(module.get_html(), str(module_system.anonymous_student_id))
...@@ -23,16 +24,17 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase): ...@@ -23,16 +24,17 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
<p>Hi USER_ID!11!</p> <p>Hi USER_ID!11!</p>
</html> </html>
''' '''
module_data = {'data': sample_xml} field_data = DictFieldData({'data': sample_xml})
module = HtmlModule(get_test_system(), self.descriptor, module_data) module_system = get_test_system()
module = HtmlModule(self.descriptor, module_system, field_data, Mock())
self.assertEqual(module.get_html(), sample_xml) self.assertEqual(module.get_html(), sample_xml)
def test_substitution_without_anonymous_student_id(self): def test_substitution_without_anonymous_student_id(self):
sample_xml = '''%%USER_ID%%''' sample_xml = '''%%USER_ID%%'''
module_data = {'data': sample_xml} field_data = DictFieldData({'data': sample_xml})
module_system = get_test_system() module_system = get_test_system()
module_system.anonymous_student_id = None module_system.anonymous_student_id = None
module = HtmlModule(module_system, self.descriptor, module_data) module = HtmlModule(self.descriptor, module_system, field_data, Mock())
self.assertEqual(module.get_html(), sample_xml) self.assertEqual(module.get_html(), sample_xml)
...@@ -15,6 +15,7 @@ from xmodule.modulestore.xml import ImportSystem, XMLModuleStore ...@@ -15,6 +15,7 @@ from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.modulestore.inheritance import compute_inherited_metadata from xmodule.modulestore.inheritance import compute_inherited_metadata
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from xmodule.modulestore.inheritance import InheritanceMixin
ORG = 'test_org' ORG = 'test_org'
...@@ -34,13 +35,14 @@ class DummySystem(ImportSystem): ...@@ -34,13 +35,14 @@ class DummySystem(ImportSystem):
parent_tracker = Mock() parent_tracker = Mock()
super(DummySystem, self).__init__( super(DummySystem, self).__init__(
xmlstore, xmlstore=xmlstore,
course_id, course_id=course_id,
course_dir, course_dir=course_dir,
policy, policy=policy,
error_tracker, error_tracker=error_tracker,
parent_tracker, parent_tracker=parent_tracker,
load_error_modules=load_error_modules, load_error_modules=load_error_modules,
mixins=(InheritanceMixin,)
) )
def render_template(self, _template, _context): def render_template(self, _template, _context):
...@@ -58,7 +60,7 @@ class BaseCourseTestCase(unittest.TestCase): ...@@ -58,7 +60,7 @@ class BaseCourseTestCase(unittest.TestCase):
"""Get a test course by directory name. If there's more than one, error.""" """Get a test course by directory name. If there's more than one, error."""
print("Importing {0}".format(name)) print("Importing {0}".format(name))
modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name]) modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name], xblock_mixins=(InheritanceMixin,))
courses = modulestore.get_courses() courses = modulestore.get_courses()
self.assertEquals(len(courses), 1) self.assertEquals(len(courses), 1)
return courses[0] return courses[0]
...@@ -76,7 +78,7 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -76,7 +78,7 @@ class ImportTestCase(BaseCourseTestCase):
descriptor = system.process_xml(bad_xml) descriptor = system.process_xml(bad_xml)
self.assertEqual(descriptor.__class__.__name__, 'ErrorDescriptor') self.assertEqual(descriptor.__class__.__name__, 'ErrorDescriptorWithMixins')
def test_unique_url_names(self): def test_unique_url_names(self):
'''Check that each error gets its very own url_name''' '''Check that each error gets its very own url_name'''
...@@ -102,7 +104,7 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -102,7 +104,7 @@ class ImportTestCase(BaseCourseTestCase):
re_import_descriptor = system.process_xml(tag_xml) re_import_descriptor = system.process_xml(tag_xml)
self.assertEqual(re_import_descriptor.__class__.__name__, self.assertEqual(re_import_descriptor.__class__.__name__,
'ErrorDescriptor') 'ErrorDescriptorWithMixins')
self.assertEqual(descriptor.contents, self.assertEqual(descriptor.contents,
re_import_descriptor.contents) re_import_descriptor.contents)
...@@ -150,15 +152,17 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -150,15 +152,17 @@ class ImportTestCase(BaseCourseTestCase):
compute_inherited_metadata(descriptor) compute_inherited_metadata(descriptor)
# pylint: disable=W0212 # pylint: disable=W0212
print(descriptor, descriptor._model_data) print(descriptor, descriptor._field_data)
self.assertEqual(descriptor.lms.due, ImportTestCase.date.from_json(v)) self.assertEqual(descriptor.due, ImportTestCase.date.from_json(v))
# Check that the child inherits due correctly # Check that the child inherits due correctly
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.lms.due, ImportTestCase.date.from_json(v)) self.assertEqual(child.due, ImportTestCase.date.from_json(v))
self.assertEqual(child._inheritable_metadata, child._inherited_metadata) # need to convert v to canonical json b4 comparing
self.assertEqual(1, len(child._inherited_metadata)) self.assertEqual(
self.assertEqual(v, child._inherited_metadata['due']) ImportTestCase.date.to_json(ImportTestCase.date.from_json(v)),
child.xblock_kvs.inherited_settings['due']
)
# Now export and check things # Now export and check things
resource_fs = MemoryFS() resource_fs = MemoryFS()
...@@ -208,15 +212,13 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -208,15 +212,13 @@ class ImportTestCase(BaseCourseTestCase):
descriptor = system.process_xml(start_xml) descriptor = system.process_xml(start_xml)
compute_inherited_metadata(descriptor) compute_inherited_metadata(descriptor)
self.assertEqual(descriptor.lms.due, None) self.assertEqual(descriptor.due, None)
# Check that the child does not inherit a value for due # Check that the child does not inherit a value for due
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.lms.due, None) self.assertEqual(child.due, None)
# pylint: disable=W0212
self.assertEqual(child._inheritable_metadata, child._inherited_metadata)
self.assertLessEqual( self.assertLessEqual(
child.lms.start, child.start,
datetime.datetime.now(UTC()) datetime.datetime.now(UTC())
) )
...@@ -238,14 +240,16 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -238,14 +240,16 @@ class ImportTestCase(BaseCourseTestCase):
descriptor = system.process_xml(start_xml) descriptor = system.process_xml(start_xml)
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
# pylint: disable=W0212 # pylint: disable=W0212
child._model_data['due'] = child_due child._field_data.set(child, 'due', child_due)
compute_inherited_metadata(descriptor) compute_inherited_metadata(descriptor)
self.assertEqual(descriptor.lms.due, ImportTestCase.date.from_json(course_due)) self.assertEqual(descriptor.due, ImportTestCase.date.from_json(course_due))
self.assertEqual(child.lms.due, ImportTestCase.date.from_json(child_due)) self.assertEqual(child.due, ImportTestCase.date.from_json(child_due))
# Test inherited metadata. Due does not appear here (because explicitly set on child). # Test inherited metadata. Due does not appear here (because explicitly set on child).
self.assertEqual(1, len(child._inheritable_metadata)) self.assertEqual(
self.assertEqual(course_due, child._inheritable_metadata['due']) ImportTestCase.date.to_json(ImportTestCase.date.from_json(course_due)),
child.xblock_kvs.inherited_settings['due']
)
def test_is_pointer_tag(self): def test_is_pointer_tag(self):
""" """
...@@ -280,14 +284,14 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -280,14 +284,14 @@ class ImportTestCase(BaseCourseTestCase):
print("Starting import") print("Starting import")
course = self.get_course('toy') course = self.get_course('toy')
def check_for_key(key, node): def check_for_key(key, node, value):
"recursive check for presence of key" "recursive check for presence of key"
print("Checking {0}".format(node.location.url())) print("Checking {0}".format(node.location.url()))
self.assertTrue(key in node._model_data) self.assertEqual(getattr(node, key), value)
for c in node.get_children(): for c in node.get_children():
check_for_key(key, c) check_for_key(key, c, value)
check_for_key('graceperiod', course) check_for_key('graceperiod', course, course.graceperiod)
def test_policy_loading(self): def test_policy_loading(self):
"""Make sure that when two courses share content with the same """Make sure that when two courses share content with the same
...@@ -310,7 +314,7 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -310,7 +314,7 @@ class ImportTestCase(BaseCourseTestCase):
# Also check that keys from policy are run through the # Also check that keys from policy are run through the
# appropriate attribute maps -- 'graded' should be True, not 'true' # appropriate attribute maps -- 'graded' should be True, not 'true'
self.assertEqual(toy.lms.graded, True) self.assertEqual(toy.graded, True)
def test_definition_loading(self): def test_definition_loading(self):
"""When two courses share the same org and course name and """When two courses share the same org and course name and
......
...@@ -7,7 +7,7 @@ from . import LogicTest ...@@ -7,7 +7,7 @@ from . import LogicTest
class PollModuleTest(LogicTest): class PollModuleTest(LogicTest):
"""Logic tests for Poll Xmodule.""" """Logic tests for Poll Xmodule."""
descriptor_class = PollDescriptor descriptor_class = PollDescriptor
raw_model_data = { raw_field_data = {
'poll_answers': {'Yes': 1, 'Dont_know': 0, 'No': 0}, 'poll_answers': {'Yes': 1, 'Dont_know': 0, 'No': 0},
'voted': False, 'voted': False,
'poll_answer': '' 'poll_answer': ''
......
"""Module progress tests""" """Module progress tests"""
import unittest import unittest
from mock import Mock
from xblock.field_data import DictFieldData
from xmodule.progress import Progress from xmodule.progress import Progress
from xmodule import x_module from xmodule import x_module
...@@ -134,6 +137,6 @@ class ModuleProgressTest(unittest.TestCase): ...@@ -134,6 +137,6 @@ class ModuleProgressTest(unittest.TestCase):
''' '''
def test_xmodule_default(self): def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None''' '''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(get_test_system(), None, {'location': 'a://b/c/d/e'}) xm = x_module.XModule(None, get_test_system(), DictFieldData({'location': 'a://b/c/d/e'}), Mock())
p = xm.get_progress() p = xm.get_progress()
self.assertEqual(p, None) self.assertEqual(p, None)
...@@ -14,21 +14,25 @@ the course, section, subsection, unit, etc. ...@@ -14,21 +14,25 @@ the course, section, subsection, unit, etc.
""" """
import unittest import unittest
from mock import Mock
from . import LogicTest from . import LogicTest
from lxml import etree from lxml import etree
from .import get_test_system
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.video_module import VideoDescriptor, _create_youtube_string from xmodule.video_module import VideoDescriptor, _create_youtube_string
from .test_import import DummySystem from .test_import import DummySystem
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from textwrap import dedent from textwrap import dedent
from xmodule.tests import get_test_descriptor_system
class VideoModuleTest(LogicTest): class VideoModuleTest(LogicTest):
"""Logic tests for Video Xmodule.""" """Logic tests for Video Xmodule."""
descriptor_class = VideoDescriptor descriptor_class = VideoDescriptor
raw_model_data = { raw_field_data = {
'data': '<video />' 'data': '<video />'
} }
...@@ -120,10 +124,12 @@ class VideoDescriptorTest(unittest.TestCase): ...@@ -120,10 +124,12 @@ class VideoDescriptorTest(unittest.TestCase):
"""Test for VideoDescriptor""" """Test for VideoDescriptor"""
def setUp(self): def setUp(self):
system = get_test_system() system = get_test_descriptor_system()
self.descriptor = VideoDescriptor( self.descriptor = system.construct_xblock_from_class(
runtime=system, VideoDescriptor,
model_data={}) field_data=DictFieldData({}),
scope_ids=ScopeIds(None, None, None, None),
)
def test_get_context(self): def test_get_context(self):
""""test get_context""" """"test get_context"""
...@@ -144,8 +150,8 @@ class VideoDescriptorTest(unittest.TestCase): ...@@ -144,8 +150,8 @@ class VideoDescriptorTest(unittest.TestCase):
""" """
system = DummySystem(load_error_modules=True) system = DummySystem(load_error_modules=True)
location = Location(["i4x", "edX", "video", "default", "SampleProblem1"]) location = Location(["i4x", "edX", "video", "default", "SampleProblem1"])
model_data = {'location': location} field_data = DictFieldData({'location': location})
descriptor = VideoDescriptor(system, model_data) descriptor = VideoDescriptor(system, field_data, Mock())
descriptor.youtube_id_0_75 = 'izygArpw-Qo' descriptor.youtube_id_0_75 = 'izygArpw-Qo'
descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8' descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8'
descriptor.youtube_id_1_25 = '1EeWXzPdhSA' descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
...@@ -160,8 +166,8 @@ class VideoDescriptorTest(unittest.TestCase): ...@@ -160,8 +166,8 @@ class VideoDescriptorTest(unittest.TestCase):
""" """
system = DummySystem(load_error_modules=True) system = DummySystem(load_error_modules=True)
location = Location(["i4x", "edX", "video", "default", "SampleProblem1"]) location = Location(["i4x", "edX", "video", "default", "SampleProblem1"])
model_data = {'location': location} field_data = DictFieldData({'location': location})
descriptor = VideoDescriptor(system, model_data) descriptor = VideoDescriptor(system, field_data, Mock())
descriptor.youtube_id_0_75 = 'izygArpw-Qo' descriptor.youtube_id_0_75 = 'izygArpw-Qo'
descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8' descriptor.youtube_id_1_0 = 'p2Q6BrNhdh8'
descriptor.youtube_id_1_25 = '1EeWXzPdhSA' descriptor.youtube_id_1_25 = '1EeWXzPdhSA'
...@@ -196,10 +202,12 @@ class VideoDescriptorImportTestCase(unittest.TestCase): ...@@ -196,10 +202,12 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
''' '''
location = Location(["i4x", "edX", "video", "default", location = Location(["i4x", "edX", "video", "default",
"SampleProblem1"]) "SampleProblem1"])
model_data = {'data': sample_xml, field_data = DictFieldData({
'location': location} 'data': sample_xml,
'location': location
})
system = DummySystem(load_error_modules=True) system = DummySystem(load_error_modules=True)
descriptor = VideoDescriptor(system, model_data) descriptor = VideoDescriptor(system, field_data, Mock())
self.assert_attributes_equal(descriptor, { self.assert_attributes_equal(descriptor, {
'youtube_id_0_75': 'izygArpw-Qo', 'youtube_id_0_75': 'izygArpw-Qo',
'youtube_id_1_0': 'p2Q6BrNhdh8', 'youtube_id_1_0': 'p2Q6BrNhdh8',
...@@ -296,7 +304,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase): ...@@ -296,7 +304,7 @@ class VideoDescriptorImportTestCase(unittest.TestCase):
a few weeks). a few weeks).
""" """
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
xml_data =''' xml_data = '''
<video display_name="&quot;display_name&quot;" <video display_name="&quot;display_name&quot;"
html5_sources="[&quot;source_1&quot;, &quot;source_2&quot;]" html5_sources="[&quot;source_1&quot;, &quot;source_2&quot;]"
show_captions="false" show_captions="false"
...@@ -410,12 +418,17 @@ class VideoExportTestCase(unittest.TestCase): ...@@ -410,12 +418,17 @@ class VideoExportTestCase(unittest.TestCase):
Make sure that VideoDescriptor can export itself to XML Make sure that VideoDescriptor can export itself to XML
correctly. correctly.
""" """
def assertXmlEqual(self, expected, xml):
for attr in ['tag', 'attrib', 'text', 'tail']:
self.assertEqual(getattr(expected, attr), getattr(xml, attr))
for left, right in zip(expected, xml):
self.assertXmlEqual(left, right)
def test_export_to_xml(self): def test_export_to_xml(self):
"""Test that we write the correct XML on export.""" """Test that we write the correct XML on export."""
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
location = Location(["i4x", "edX", "video", "default", "SampleProblem1"]) location = Location(["i4x", "edX", "video", "default", "SampleProblem1"])
desc = VideoDescriptor(module_system, {'location': location}) desc = VideoDescriptor(module_system, DictFieldData({}), ScopeIds(None, None, location, location))
desc.youtube_id_0_75 = 'izygArpw-Qo' desc.youtube_id_0_75 = 'izygArpw-Qo'
desc.youtube_id_1_0 = 'p2Q6BrNhdh8' desc.youtube_id_1_0 = 'p2Q6BrNhdh8'
...@@ -428,7 +441,7 @@ class VideoExportTestCase(unittest.TestCase): ...@@ -428,7 +441,7 @@ class VideoExportTestCase(unittest.TestCase):
desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg'] desc.html5_sources = ['http://www.example.com/source.mp4', 'http://www.example.com/source.ogg']
xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter xml = desc.definition_to_xml(None) # We don't use the `resource_fs` parameter
expected = dedent('''\ expected = etree.fromstring('''\
<video url_name="SampleProblem1" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00"> <video url_name="SampleProblem1" start_time="0:00:01" youtube="0.75:izygArpw-Qo,1.00:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" show_captions="false" end_time="0:01:00">
<source src="http://www.example.com/source.mp4"/> <source src="http://www.example.com/source.mp4"/>
<source src="http://www.example.com/source.ogg"/> <source src="http://www.example.com/source.ogg"/>
...@@ -436,13 +449,13 @@ class VideoExportTestCase(unittest.TestCase): ...@@ -436,13 +449,13 @@ class VideoExportTestCase(unittest.TestCase):
</video> </video>
''') ''')
self.assertEquals(expected, etree.tostring(xml, pretty_print=True)) self.assertXmlEqual(expected, xml)
def test_export_to_xml_empty_parameters(self): def test_export_to_xml_empty_parameters(self):
"""Test XML export with defaults.""" """Test XML export with defaults."""
module_system = DummySystem(load_error_modules=True) module_system = DummySystem(load_error_modules=True)
location = Location(["i4x", "edX", "video", "default", "SampleProblem1"]) location = Location(["i4x", "edX", "video", "default", "SampleProblem1"])
desc = VideoDescriptor(module_system, {'location': location}) desc = VideoDescriptor(module_system, DictFieldData({}), ScopeIds(None, None, location, location))
xml = desc.definition_to_xml(None) xml = desc.definition_to_xml(None)
expected = '<video url_name="SampleProblem1"/>\n' expected = '<video url_name="SampleProblem1"/>\n'
......
...@@ -8,7 +8,7 @@ from . import PostData, LogicTest ...@@ -8,7 +8,7 @@ from . import PostData, LogicTest
class WordCloudModuleTest(LogicTest): class WordCloudModuleTest(LogicTest):
"""Logic tests for Word Cloud Xmodule.""" """Logic tests for Word Cloud Xmodule."""
descriptor_class = WordCloudDescriptor descriptor_class = WordCloudDescriptor
raw_model_data = { raw_field_data = {
'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2}, 'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2},
'top_words': {'cat': 10, 'dog': 5, 'dad': 2}, 'top_words': {'cat': 10, 'dog': 5, 'dad': 2},
'submitted': False 'submitted': False
......
...@@ -7,6 +7,11 @@ from nose.tools import assert_equal # pylint: disable=E0611 ...@@ -7,6 +7,11 @@ from nose.tools import assert_equal # pylint: disable=E0611
from unittest.case import SkipTest from unittest.case import SkipTest
from mock import Mock from mock import Mock
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xmodule.x_module import ModuleSystem
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.annotatable_module import AnnotatableDescriptor from xmodule.annotatable_module import AnnotatableDescriptor
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
...@@ -24,6 +29,7 @@ from xmodule.conditional_module import ConditionalDescriptor ...@@ -24,6 +29,7 @@ from xmodule.conditional_module import ConditionalDescriptor
from xmodule.randomize_module import RandomizeDescriptor from xmodule.randomize_module import RandomizeDescriptor
from xmodule.vertical_module import VerticalDescriptor from xmodule.vertical_module import VerticalDescriptor
from xmodule.wrapper_module import WrapperDescriptor from xmodule.wrapper_module import WrapperDescriptor
from xmodule.tests import get_test_descriptor_system
LEAF_XMODULES = ( LEAF_XMODULES = (
AnnotatableDescriptor, AnnotatableDescriptor,
...@@ -63,26 +69,26 @@ class TestXBlockWrapper(object): ...@@ -63,26 +69,26 @@ class TestXBlockWrapper(object):
@property @property
def leaf_module_runtime(self): def leaf_module_runtime(self):
runtime = Mock() runtime = ModuleSystem(
runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs) render_template=lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs),
runtime.anonymous_student_id = 'dummy_anonymous_student_id' anonymous_student_id='dummy_anonymous_student_id',
runtime.open_ended_grading_interface = {} open_ended_grading_interface={},
runtime.seed = 5 ajax_url='dummy_ajax_url',
runtime.get = lambda x: getattr(runtime, x) xblock_field_data=lambda d: d._field_data,
runtime.ajax_url = 'dummy_ajax_url' get_module=Mock(),
runtime.xblock_model_data = lambda d: d._model_data replace_urls=Mock(),
return runtime track_function=Mock(),
)
@property
def leaf_descriptor_runtime(self):
runtime = Mock()
runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
return runtime return runtime
def leaf_descriptor(self, descriptor_cls): def leaf_descriptor(self, descriptor_cls):
return descriptor_cls( location = 'i4x://org/course/category/name'
self.leaf_descriptor_runtime, runtime = get_test_descriptor_system()
{'location': 'i4x://org/course/category/name'} runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
return runtime.construct_xblock_from_class(
descriptor_cls,
DictFieldData({}),
ScopeIds(None, descriptor_cls.__name__, location, location)
) )
def leaf_module(self, descriptor_cls): def leaf_module(self, descriptor_cls):
...@@ -93,23 +99,20 @@ class TestXBlockWrapper(object): ...@@ -93,23 +99,20 @@ class TestXBlockWrapper(object):
if depth == 0: if depth == 0:
runtime.get_module.side_effect = lambda x: self.leaf_module(HtmlDescriptor) runtime.get_module.side_effect = lambda x: self.leaf_module(HtmlDescriptor)
else: else:
runtime.get_module.side_effect = lambda x: self.container_module(VerticalDescriptor, depth-1) runtime.get_module.side_effect = lambda x: self.container_module(VerticalDescriptor, depth - 1)
runtime.position = 2 runtime.position = 2
return runtime return runtime
@property
def container_descriptor_runtime(self):
runtime = Mock()
runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
return runtime
def container_descriptor(self, descriptor_cls): def container_descriptor(self, descriptor_cls):
return descriptor_cls( location = 'i4x://org/course/category/name'
self.container_descriptor_runtime, runtime = get_test_descriptor_system()
{ runtime.render_template = lambda *args, **kwargs: u'{!r}, {!r}'.format(args, kwargs)
'location': 'i4x://org/course/category/name', return runtime.construct_xblock_from_class(
descriptor_cls,
DictFieldData({
'children': range(3) 'children': range(3)
} }),
ScopeIds(None, descriptor_cls.__name__, location, location)
) )
def container_module(self, descriptor_cls, depth): def container_module(self, descriptor_cls, depth):
...@@ -185,9 +188,9 @@ class TestStudioView(TestXBlockWrapper): ...@@ -185,9 +188,9 @@ class TestStudioView(TestXBlockWrapper):
# Test that for all of the Descriptors listed in CONTAINER_XMODULES # Test that for all of the Descriptors listed in CONTAINER_XMODULES
# render the same thing using studio_view as they do using get_html, under the following conditions: # render the same thing using studio_view as they do using get_html, under the following conditions:
# a) All of its descendents are xmodules # a) All of its descendants are xmodules
# b) Some of its descendents are xmodules and some are xblocks # b) Some of its descendants are xmodules and some are xblocks
# c) All of its descendents are xblocks # c) All of its descendants are xblocks
def test_studio_view_container_node(self): def test_studio_view_container_node(self):
for descriptor_cls in CONTAINER_XMODULES: for descriptor_cls in CONTAINER_XMODULES:
yield self.check_studio_view_container_node_xmodules_only, descriptor_cls yield self.check_studio_view_container_node_xmodules_only, descriptor_cls
......
...@@ -8,7 +8,7 @@ from xmodule.xml_module import XmlDescriptor ...@@ -8,7 +8,7 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.progress import Progress from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xblock.core import Float, String, Boolean, Scope from xblock.fields import Float, String, Boolean, Scope
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
......
...@@ -15,6 +15,8 @@ import logging ...@@ -15,6 +15,8 @@ import logging
from lxml import etree from lxml import etree
from pkg_resources import resource_string from pkg_resources import resource_string
import datetime
import time
from django.http import Http404 from django.http import Http404
from django.conf import settings from django.conf import settings
...@@ -24,10 +26,12 @@ from xmodule.editing_module import TabsEditingDescriptor ...@@ -24,10 +26,12 @@ from xmodule.editing_module import TabsEditingDescriptor
from xmodule.raw_module import EmptyDataRawDescriptor from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.xml_module import is_pointer_tag, name_to_pathname from xmodule.xml_module import is_pointer_tag, name_to_pathname
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xblock.core import Scope, String, Boolean, Float, List, Integer from xblock.fields import Scope, String, Boolean, Float, List, Integer, ScopeIds
import datetime from xblock.field_data import DictFieldData
import time
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
from xblock.runtime import DbModel
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -98,7 +102,6 @@ class VideoFields(object): ...@@ -98,7 +102,6 @@ class VideoFields(object):
help="A list of filenames to be used with HTML5 video. The first supported filetype will be displayed.", help="A list of filenames to be used with HTML5 video. The first supported filetype will be displayed.",
display_name="Video Sources", display_name="Video Sources",
scope=Scope.settings, scope=Scope.settings,
default=[]
) )
track = String( track = String(
help="The external URL to download the timed transcript track. This appears as a link beneath the video.", help="The external URL to download the timed transcript track. This appears as a link beneath the video.",
...@@ -213,8 +216,8 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ...@@ -213,8 +216,8 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
# For backwards compatibility -- if we've got XML data, parse # For backwards compatibility -- if we've got XML data, parse
# it out and set the metadata fields # it out and set the metadata fields
if self.data: if self.data:
model_data = VideoDescriptor._parse_video_xml(self.data) field_data = VideoDescriptor._parse_video_xml(self.data)
self._model_data.update(model_data) self._field_data.set_many(self, field_data)
del self.data del self.data
@classmethod @classmethod
...@@ -237,9 +240,19 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ...@@ -237,9 +240,19 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
if is_pointer_tag(xml_object): if is_pointer_tag(xml_object):
filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name)) filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name))
xml_data = etree.tostring(cls.load_file(filepath, system.resources_fs, location)) xml_data = etree.tostring(cls.load_file(filepath, system.resources_fs, location))
model_data = VideoDescriptor._parse_video_xml(xml_data) field_data = VideoDescriptor._parse_video_xml(xml_data)
model_data['location'] = location field_data['location'] = location
video = cls(system, model_data) kvs = InheritanceKeyValueStore(initial_values=field_data)
field_data = DbModel(kvs)
video = system.construct_xblock_from_class(
cls,
field_data,
# We're loading a descriptor, so student_id is meaningless
# We also don't have separate notions of definition and usage ids yet,
# so we use the location for both
ScopeIds(None, location.category, location, location)
)
return video return video
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
...@@ -250,25 +263,22 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ...@@ -250,25 +263,22 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
youtube_string = _create_youtube_string(self) youtube_string = _create_youtube_string(self)
# Mild workaround to ensure that tests pass -- if a field # Mild workaround to ensure that tests pass -- if a field
# is set to its default value, we don't need to write it out. # is set to its default value, we don't need to write it out.
if youtube_string == '1.00:OEoXaMPEzfM': if youtube_string and youtube_string != '1.00:OEoXaMPEzfM':
youtube_string = '' xml.set('youtube', unicode(youtube_string))
xml.set('url_name', self.url_name)
attrs = { attrs = {
'display_name': self.display_name, 'display_name': self.display_name,
'show_captions': json.dumps(self.show_captions), 'show_captions': json.dumps(self.show_captions),
'youtube': youtube_string,
'start_time': datetime.timedelta(seconds=self.start_time), 'start_time': datetime.timedelta(seconds=self.start_time),
'end_time': datetime.timedelta(seconds=self.end_time), 'end_time': datetime.timedelta(seconds=self.end_time),
'sub': self.sub, 'sub': self.sub,
'url_name': self.url_name
} }
fields = {field.name: field for field in self.fields}
for key, value in attrs.items(): for key, value in attrs.items():
# Mild workaround to ensure that tests pass -- if a field # Mild workaround to ensure that tests pass -- if a field
# is set to its default value, we don't need to write it out. # is set to its default value, we don't write it out.
if key in fields and fields[key].default == getattr(self, key):
continue
if value: if value:
xml.set(key, unicode(value)) if key in self.fields and self.fields[key].is_set_on(self):
xml.set(key, unicode(value))
for source in self.html5_sources: for source in self.html5_sources:
ele = etree.Element('source') ele = etree.Element('source')
...@@ -312,7 +322,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ...@@ -312,7 +322,7 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
present in the XML. present in the XML.
""" """
xml = etree.fromstring(xml_data) xml = etree.fromstring(xml_data)
model_data = {} field_data = {}
conversions = { conversions = {
'start_time': VideoDescriptor._parse_time, 'start_time': VideoDescriptor._parse_time,
...@@ -328,12 +338,12 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ...@@ -328,12 +338,12 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
sources = xml.findall('source') sources = xml.findall('source')
if sources: if sources:
model_data['html5_sources'] = [ele.get('src') for ele in sources] field_data['html5_sources'] = [ele.get('src') for ele in sources]
model_data['source'] = model_data['html5_sources'][0] field_data['source'] = field_data['html5_sources'][0]
track = xml.find('track') track = xml.find('track')
if track is not None: if track is not None:
model_data['track'] = track.get('src') field_data['track'] = track.get('src')
for attr, value in xml.items(): for attr, value in xml.items():
if attr in compat_keys: if attr in compat_keys:
...@@ -347,8 +357,8 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ...@@ -347,8 +357,8 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
# cleanliness, but hindsight doesn't need glasses # cleanliness, but hindsight doesn't need glasses
normalized_speed = speed[:-1] if speed.endswith('0') else speed normalized_speed = speed[:-1] if speed.endswith('0') else speed
# If the user has specified html5 sources, make sure we don't use the default video # If the user has specified html5 sources, make sure we don't use the default video
if youtube_id != '' or 'html5_sources' in model_data: if youtube_id != '' or 'html5_sources' in field_data:
model_data['youtube_id_{0}'.format(normalized_speed.replace('.', '_'))] = youtube_id field_data['youtube_id_{0}'.format(normalized_speed.replace('.', '_'))] = youtube_id
else: else:
# Convert XML attrs into Python values. # Convert XML attrs into Python values.
if attr in conversions: if attr in conversions:
...@@ -357,9 +367,9 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor ...@@ -357,9 +367,9 @@ class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor
# We export values with json.dumps (well, except for Strings, but # We export values with json.dumps (well, except for Strings, but
# for about a month we did it for Strings also). # for about a month we did it for Strings also).
value = VideoDescriptor._deserialize(attr, value) value = VideoDescriptor._deserialize(attr, value)
model_data[attr] = value field_data[attr] = value
return model_data return field_data
@classmethod @classmethod
def _deserialize(cls, attr, value): def _deserialize(cls, attr, value):
......
...@@ -14,7 +14,7 @@ from xmodule.raw_module import EmptyDataRawDescriptor ...@@ -14,7 +14,7 @@ from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xblock.core import Scope, Dict, Boolean, List, Integer, String from xblock.fields import Scope, Dict, Boolean, List, Integer, String
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -71,11 +71,11 @@ class WordCloudFields(object): ...@@ -71,11 +71,11 @@ class WordCloudFields(object):
) )
all_words = Dict( all_words = Dict(
help="All possible words from all students.", help="All possible words from all students.",
scope=Scope.content scope=Scope.user_state_summary
) )
top_words = Dict( top_words = Dict(
help="Top num_top_words words for word cloud.", help="Top num_top_words words for word cloud.",
scope=Scope.content scope=Scope.user_state_summary
) )
......
...@@ -6,11 +6,12 @@ import sys ...@@ -6,11 +6,12 @@ import sys
from collections import namedtuple from collections import namedtuple
from lxml import etree from lxml import etree
from xblock.core import Dict, Scope from xblock.fields import Dict, Scope, ScopeIds
from xmodule.x_module import (XModuleDescriptor, policy_key) from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata, InheritanceKeyValueStore
from xmodule.modulestore.xml_exporter import EdxJSONEncoder from xmodule.modulestore.xml_exporter import EdxJSONEncoder
from xblock.runtime import DbModel
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -172,13 +173,12 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -172,13 +173,12 @@ class XmlDescriptor(XModuleDescriptor):
Searches through fields defined by cls to find one named attr. Searches through fields defined by cls to find one named attr.
""" """
for field in set(cls.fields + cls.lms.fields): if attr in cls.fields:
if field.name == attr: from_xml = lambda val: deserialize_field(cls.fields[attr], val)
from_xml = lambda val: deserialize_field(field, val) to_xml = lambda val: serialize_field(val)
to_xml = lambda val: serialize_field(val) return AttrMap(from_xml, to_xml)
return AttrMap(from_xml, to_xml) else:
return AttrMap()
return AttrMap()
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
...@@ -352,22 +352,29 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -352,22 +352,29 @@ class XmlDescriptor(XModuleDescriptor):
if k in system.policy: if k in system.policy:
cls.apply_policy(metadata, system.policy[k]) cls.apply_policy(metadata, system.policy[k])
model_data = {} field_data = {}
model_data.update(metadata) field_data.update(metadata)
model_data.update(definition) field_data.update(definition)
model_data['children'] = children field_data['children'] = children
model_data['xml_attributes'] = {} field_data['xml_attributes'] = {}
model_data['xml_attributes']['filename'] = definition.get('filename', ['', None]) # for git link field_data['xml_attributes']['filename'] = definition.get('filename', ['', None]) # for git link
for key, value in metadata.items(): for key, value in metadata.items():
if key not in set(f.name for f in cls.fields + cls.lms.fields): if key not in cls.fields:
model_data['xml_attributes'][key] = value field_data['xml_attributes'][key] = value
model_data['location'] = location field_data['location'] = location
model_data['category'] = xml_object.tag field_data['category'] = xml_object.tag
kvs = InheritanceKeyValueStore(initial_values=field_data)
return cls( field_data = DbModel(kvs)
system,
model_data, return system.construct_xblock_from_class(
cls,
field_data,
# We're loading a descriptor, so student_id is meaningless
# We also don't have separate notions of definition and usage ids yet,
# so we use the location for both
ScopeIds(None, location.category, location, location)
) )
@classmethod @classmethod
...@@ -413,7 +420,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -413,7 +420,7 @@ class XmlDescriptor(XModuleDescriptor):
(Possible format conversion through an AttrMap). (Possible format conversion through an AttrMap).
""" """
attr_map = self.get_map_for_field(attr) attr_map = self.get_map_for_field(attr)
return attr_map.to_xml(self._model_data[attr]) return attr_map.to_xml(self._field_data.get(self, attr))
# Add the non-inherited metadata # Add the non-inherited metadata
for attr in sorted(own_metadata(self)): for attr in sorted(own_metadata(self)):
......
...@@ -100,6 +100,9 @@ pygments_style = 'sphinx' ...@@ -100,6 +100,9 @@ pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
#modindex_common_prefix = [] #modindex_common_prefix = []
# When auto-doc'ing a class, write the class' docstring and the __init__ docstring
# into the class docs.
autoclass_content = "both"
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------
......
...@@ -80,7 +80,7 @@ def dump_grading_context(course): ...@@ -80,7 +80,7 @@ def dump_grading_context(course):
msg += "--> Section %s:\n" % (gsomething) msg += "--> Section %s:\n" % (gsomething)
for sec in gsvals: for sec in gsvals:
sdesc = sec['section_descriptor'] sdesc = sec['section_descriptor']
frmat = getattr(sdesc.lms, 'format', None) frmat = getattr(sdesc, 'format', None)
aname = '' aname = ''
if frmat in graders: if frmat in graders:
gform = graders[frmat] gform = graders[frmat]
......
...@@ -143,12 +143,12 @@ def _has_access_course_desc(user, course, action): ...@@ -143,12 +143,12 @@ def _has_access_course_desc(user, course, action):
""" """
First check if restriction of enrollment by login method is enabled, both First check if restriction of enrollment by login method is enabled, both
globally and by the course. globally and by the course.
If it is, then the user must pass the criterion set by the course, e.g. that ExternalAuthMap If it is, then the user must pass the criterion set by the course, e.g. that ExternalAuthMap
was set by 'shib:https://idp.stanford.edu/", in addition to requirements below. was set by 'shib:https://idp.stanford.edu/", in addition to requirements below.
Rest of requirements: Rest of requirements:
Enrollment can only happen in the course enrollment period, if one exists. Enrollment can only happen in the course enrollment period, if one exists.
or or
(CourseEnrollmentAllowed always overrides) (CourseEnrollmentAllowed always overrides)
(staff can always enroll) (staff can always enroll)
""" """
...@@ -195,7 +195,7 @@ def _has_access_course_desc(user, course, action): ...@@ -195,7 +195,7 @@ def _has_access_course_desc(user, course, action):
if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'): if settings.MITX_FEATURES.get('ACCESS_REQUIRE_STAFF_FOR_COURSE'):
# if this feature is on, only allow courses that have ispublic set to be # if this feature is on, only allow courses that have ispublic set to be
# seen by non-staff # seen by non-staff
if course.lms.ispublic: if course.ispublic:
debug("Allow: ACCESS_REQUIRE_STAFF_FOR_COURSE and ispublic") debug("Allow: ACCESS_REQUIRE_STAFF_FOR_COURSE and ispublic")
return True return True
return _has_staff_access_to_descriptor(user, course) return _has_staff_access_to_descriptor(user, course)
...@@ -272,7 +272,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None): ...@@ -272,7 +272,7 @@ def _has_access_descriptor(user, descriptor, action, course_context=None):
return True return True
# Check start date # Check start date
if descriptor.lms.start is not None: if descriptor.start is not None:
now = datetime.now(UTC()) now = datetime.now(UTC())
effective_start = _adjust_start_date_for_beta_testers(user, descriptor) effective_start = _adjust_start_date_for_beta_testers(user, descriptor)
if now > effective_start: if now > effective_start:
...@@ -526,20 +526,20 @@ def _adjust_start_date_for_beta_testers(user, descriptor): ...@@ -526,20 +526,20 @@ def _adjust_start_date_for_beta_testers(user, descriptor):
NOTE: If testing manually, make sure MITX_FEATURES['DISABLE_START_DATES'] = False NOTE: If testing manually, make sure MITX_FEATURES['DISABLE_START_DATES'] = False
in envs/dev.py! in envs/dev.py!
""" """
if descriptor.lms.days_early_for_beta is None: if descriptor.days_early_for_beta is None:
# bail early if no beta testing is set up # bail early if no beta testing is set up
return descriptor.lms.start return descriptor.start
user_groups = [g.name for g in user.groups.all()] user_groups = [g.name for g in user.groups.all()]
beta_group = course_beta_test_group_name(descriptor.location) beta_group = course_beta_test_group_name(descriptor.location)
if beta_group in user_groups: if beta_group in user_groups:
debug("Adjust start time: user in group %s", beta_group) debug("Adjust start time: user in group %s", beta_group)
delta = timedelta(descriptor.lms.days_early_for_beta) delta = timedelta(descriptor.days_early_for_beta)
effective = descriptor.lms.start - delta effective = descriptor.start - delta
return effective return effective
return descriptor.lms.start return descriptor.start
def _has_instructor_access_to_location(user, location, course_context=None): def _has_instructor_access_to_location(user, location, course_context=None):
......
...@@ -13,7 +13,7 @@ from xmodule.modulestore import Location, XML_MODULESTORE_TYPE ...@@ -13,7 +13,7 @@ from xmodule.modulestore import Location, XML_MODULESTORE_TYPE
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from courseware.model_data import ModelDataCache from courseware.model_data import FieldDataCache
from static_replace import replace_static_urls from static_replace import replace_static_urls
from courseware.access import has_access from courseware.access import has_access
import branding import branding
...@@ -82,8 +82,8 @@ def get_opt_course_with_access(user, course_id, action): ...@@ -82,8 +82,8 @@ def get_opt_course_with_access(user, course_id, action):
def course_image_url(course): def course_image_url(course):
"""Try to look up the image url for the course. If it's not found, """Try to look up the image url for the course. If it's not found,
log an error and return the dead link""" log an error and return the dead link"""
if course.lms.static_asset_path or modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE: if course.static_asset_path or modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE:
return '/static/' + (course.lms.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg" return '/static/' + (course.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg"
else: else:
loc = course.location._replace(tag='c4x', category='asset', name=course.course_image) loc = course.location._replace(tag='c4x', category='asset', name=course.course_image)
_path = StaticContent.get_url_path_from_location(loc) _path = StaticContent.get_url_path_from_location(loc)
...@@ -149,16 +149,16 @@ def get_course_about_section(course, section_key): ...@@ -149,16 +149,16 @@ def get_course_about_section(course, section_key):
loc = course.location._replace(category='about', name=section_key) loc = course.location._replace(category='about', name=section_key)
# Use an empty cache # Use an empty cache
model_data_cache = ModelDataCache([], course.id, request.user) field_data_cache = FieldDataCache([], course.id, request.user)
about_module = get_module( about_module = get_module(
request.user, request.user,
request, request,
loc, loc,
model_data_cache, field_data_cache,
course.id, course.id,
not_found_ok=True, not_found_ok=True,
wrap_xmodule_display=False, wrap_xmodule_display=False,
static_asset_path=course.lms.static_asset_path static_asset_path=course.static_asset_path
) )
html = '' html = ''
...@@ -199,15 +199,15 @@ def get_course_info_section(request, course, section_key): ...@@ -199,15 +199,15 @@ def get_course_info_section(request, course, section_key):
loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key) loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
# Use an empty cache # Use an empty cache
model_data_cache = ModelDataCache([], course.id, request.user) field_data_cache = FieldDataCache([], course.id, request.user)
info_module = get_module( info_module = get_module(
request.user, request.user,
request, request,
loc, loc,
model_data_cache, field_data_cache,
course.id, course.id,
wrap_xmodule_display=False, wrap_xmodule_display=False,
static_asset_path=course.lms.static_asset_path static_asset_path=course.static_asset_path
) )
html = '' html = ''
...@@ -246,7 +246,7 @@ def get_course_syllabus_section(course, section_key): ...@@ -246,7 +246,7 @@ def get_course_syllabus_section(course, section_key):
htmlFile.read().decode('utf-8'), htmlFile.read().decode('utf-8'),
getattr(course, 'data_dir', None), getattr(course, 'data_dir', None),
course_id=course.location.course_id, course_id=course.location.course_id,
static_asset_path=course.lms.static_asset_path, static_asset_path=course.static_asset_path,
) )
except ResourceNotFoundError: except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format( log.exception("Missing syllabus section {key} in course {url}".format(
......
...@@ -111,7 +111,7 @@ def get_courseware_with_tabs(course_id): ...@@ -111,7 +111,7 @@ def get_courseware_with_tabs(course_id):
the tabs on the right hand main navigation page. the tabs on the right hand main navigation page.
This hides the appropriate courseware as defined by the hide_from_toc field: This hides the appropriate courseware as defined by the hide_from_toc field:
chapter.lms.hide_from_toc chapter.hide_from_toc
Example: Example:
...@@ -164,14 +164,14 @@ def get_courseware_with_tabs(course_id): ...@@ -164,14 +164,14 @@ def get_courseware_with_tabs(course_id):
""" """
course = get_course_by_id(course_id) course = get_course_by_id(course_id)
chapters = [chapter for chapter in course.get_children() if not chapter.lms.hide_from_toc] chapters = [chapter for chapter in course.get_children() if not chapter.hide_from_toc]
courseware = [{'chapter_name': c.display_name_with_default, courseware = [{'chapter_name': c.display_name_with_default,
'sections': [{'section_name': s.display_name_with_default, 'sections': [{'section_name': s.display_name_with_default,
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0, 'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0, 'tabs': [{'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0,
'class': t.__class__.__name__} 'class': t.__class__.__name__}
for t in s.get_children()]} for t in s.get_children()]}
for s in c.get_children() if not s.lms.hide_from_toc]} for s in c.get_children() if not s.hide_from_toc]}
for c in chapters] for c in chapters]
return courseware return courseware
...@@ -8,8 +8,8 @@ from collections import defaultdict ...@@ -8,8 +8,8 @@ from collections import defaultdict
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .model_data import ModelDataCache, LmsKeyValueStore from courseware.model_data import FieldDataCache, DjangoKeyValueStore
from xblock.core import Scope from xblock.fields import Scope
from .module_render import get_module, get_module_for_descriptor from .module_render import get_module, get_module_for_descriptor
from xmodule import graders from xmodule import graders
from xmodule.capa_module import CapaModule from xmodule.capa_module import CapaModule
...@@ -75,10 +75,10 @@ def yield_problems(request, course, student): ...@@ -75,10 +75,10 @@ def yield_problems(request, course, student):
sections_to_list.append(section_descriptor) sections_to_list.append(section_descriptor)
break break
model_data_cache = ModelDataCache(sections_to_list, course.id, student) field_data_cache = FieldDataCache(sections_to_list, course.id, student)
for section_descriptor in sections_to_list: for section_descriptor in sections_to_list:
section_module = get_module(student, request, section_module = get_module(student, request,
section_descriptor.location, model_data_cache, section_descriptor.location, field_data_cache,
course.id) course.id)
if section_module is None: if section_module is None:
# student doesn't have access to this module, or something else # student doesn't have access to this module, or something else
...@@ -119,7 +119,7 @@ def answer_distributions(request, course): ...@@ -119,7 +119,7 @@ def answer_distributions(request, course):
return counts return counts
def grade(student, request, course, model_data_cache=None, keep_raw_scores=False): def grade(student, request, course, field_data_cache=None, keep_raw_scores=False):
""" """
This grades a student as quickly as possible. It returns the This grades a student as quickly as possible. It returns the
output from the course grader, augmented with the final letter output from the course grader, augmented with the final letter
...@@ -141,8 +141,8 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -141,8 +141,8 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
grading_context = course.grading_context grading_context = course.grading_context
raw_scores = [] raw_scores = []
if model_data_cache is None: if field_data_cache is None:
model_data_cache = ModelDataCache(grading_context['all_descriptors'], course.id, student) field_data_cache = FieldDataCache(grading_context['all_descriptors'], course.id, student)
totaled_scores = {} totaled_scores = {}
# This next complicated loop is just to collect the totaled_scores, which is # This next complicated loop is just to collect the totaled_scores, which is
...@@ -162,15 +162,15 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -162,15 +162,15 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
should_grade_section = True should_grade_section = True
break break
# Create a fake key to pull out a StudentModule object from the ModelDataCache # Create a fake key to pull out a StudentModule object from the FieldDataCache
key = LmsKeyValueStore.Key( key = DjangoKeyValueStore.Key(
Scope.user_state, Scope.user_state,
student.id, student.id,
moduledescriptor.location, moduledescriptor.location,
None None
) )
if model_data_cache.find(key): if field_data_cache.find(key):
should_grade_section = True should_grade_section = True
break break
...@@ -181,11 +181,11 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -181,11 +181,11 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
'''creates an XModule instance given a descriptor''' '''creates an XModule instance given a descriptor'''
# TODO: We need the request to pass into here. If we could forego that, our arguments # TODO: We need the request to pass into here. If we could forego that, our arguments
# would be simpler # would be simpler
return get_module_for_descriptor(student, request, descriptor, model_data_cache, course.id) return get_module_for_descriptor(student, request, descriptor, field_data_cache, course.id)
for module_descriptor in yield_dynamic_descriptor_descendents(section_descriptor, create_module): for module_descriptor in yield_dynamic_descriptor_descendents(section_descriptor, create_module):
(correct, total) = get_score(course.id, student, module_descriptor, create_module, model_data_cache) (correct, total) = get_score(course.id, student, module_descriptor, create_module, field_data_cache)
if correct is None and total is None: if correct is None and total is None:
continue continue
...@@ -195,7 +195,7 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -195,7 +195,7 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
else: else:
correct = total correct = total
graded = module_descriptor.lms.graded graded = module_descriptor.graded
if not total > 0: if not total > 0:
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage #We simply cannot grade a problem that is 12/0, because we might need it as a percentage
graded = False graded = False
...@@ -257,7 +257,7 @@ def grade_for_percentage(grade_cutoffs, percentage): ...@@ -257,7 +257,7 @@ def grade_for_percentage(grade_cutoffs, percentage):
# TODO: This method is not very good. It was written in the old course style and # TODO: This method is not very good. It was written in the old course style and
# then converted over and performance is not good. Once the progress page is redesigned # then converted over and performance is not good. Once the progress page is redesigned
# to not have the progress summary this method should be deleted (so it won't be copied). # to not have the progress summary this method should be deleted (so it won't be copied).
def progress_summary(student, request, course, model_data_cache): def progress_summary(student, request, course, field_data_cache):
""" """
This pulls a summary of all problems in the course. This pulls a summary of all problems in the course.
...@@ -271,7 +271,7 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -271,7 +271,7 @@ def progress_summary(student, request, course, model_data_cache):
Arguments: Arguments:
student: A User object for the student to grade student: A User object for the student to grade
course: A Descriptor containing the course to grade course: A Descriptor containing the course to grade
model_data_cache: A ModelDataCache initialized with all field_data_cache: A FieldDataCache initialized with all
instance_modules for the student instance_modules for the student
If the student does not have access to load the course module, this function If the student does not have access to load the course module, this function
...@@ -281,7 +281,7 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -281,7 +281,7 @@ def progress_summary(student, request, course, model_data_cache):
# TODO: We need the request to pass into here. If we could forego that, our arguments # TODO: We need the request to pass into here. If we could forego that, our arguments
# would be simpler # would be simpler
course_module = get_module(student, request, course.location, model_data_cache, course.id, depth=None) course_module = get_module(student, request, course.location, field_data_cache, course.id, depth=None)
if not course_module: if not course_module:
# This student must not have access to the course. # This student must not have access to the course.
return None return None
...@@ -290,17 +290,17 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -290,17 +290,17 @@ def progress_summary(student, request, course, model_data_cache):
# Don't include chapters that aren't displayable (e.g. due to error) # Don't include chapters that aren't displayable (e.g. due to error)
for chapter_module in course_module.get_display_items(): for chapter_module in course_module.get_display_items():
# Skip if the chapter is hidden # Skip if the chapter is hidden
if chapter_module.lms.hide_from_toc: if chapter_module.hide_from_toc:
continue continue
sections = [] sections = []
for section_module in chapter_module.get_display_items(): for section_module in chapter_module.get_display_items():
# Skip if the section is hidden # Skip if the section is hidden
if section_module.lms.hide_from_toc: if section_module.hide_from_toc:
continue continue
# Same for sections # Same for sections
graded = section_module.lms.graded graded = section_module.graded
scores = [] scores = []
module_creator = section_module.system.get_module module_creator = section_module.system.get_module
...@@ -308,7 +308,7 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -308,7 +308,7 @@ def progress_summary(student, request, course, model_data_cache):
for module_descriptor in yield_dynamic_descriptor_descendents(section_module.descriptor, module_creator): for module_descriptor in yield_dynamic_descriptor_descendents(section_module.descriptor, module_creator):
course_id = course.id course_id = course.id
(correct, total) = get_score(course_id, student, module_descriptor, module_creator, model_data_cache) (correct, total) = get_score(course_id, student, module_descriptor, module_creator, field_data_cache)
if correct is None and total is None: if correct is None and total is None:
continue continue
...@@ -318,14 +318,14 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -318,14 +318,14 @@ def progress_summary(student, request, course, model_data_cache):
section_total, _ = graders.aggregate_scores( section_total, _ = graders.aggregate_scores(
scores, section_module.display_name_with_default) scores, section_module.display_name_with_default)
module_format = section_module.lms.format if section_module.lms.format is not None else '' module_format = section_module.format if section_module.format is not None else ''
sections.append({ sections.append({
'display_name': section_module.display_name_with_default, 'display_name': section_module.display_name_with_default,
'url_name': section_module.url_name, 'url_name': section_module.url_name,
'scores': scores, 'scores': scores,
'section_total': section_total, 'section_total': section_total,
'format': module_format, 'format': module_format,
'due': section_module.lms.due, 'due': section_module.due,
'graded': graded, 'graded': graded,
}) })
...@@ -337,7 +337,7 @@ def progress_summary(student, request, course, model_data_cache): ...@@ -337,7 +337,7 @@ def progress_summary(student, request, course, model_data_cache):
return chapters return chapters
def get_score(course_id, user, problem_descriptor, module_creator, model_data_cache): def get_score(course_id, user, problem_descriptor, module_creator, field_data_cache):
""" """
Return the score for a user on a problem, as a tuple (correct, total). Return the score for a user on a problem, as a tuple (correct, total).
e.g. (5,7) if you got 5 out of 7 points. e.g. (5,7) if you got 5 out of 7 points.
...@@ -349,7 +349,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca ...@@ -349,7 +349,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
problem_descriptor: an XModuleDescriptor problem_descriptor: an XModuleDescriptor
module_creator: a function that takes a descriptor, and returns the corresponding XModule for this user. module_creator: a function that takes a descriptor, and returns the corresponding XModule for this user.
Can return None if user doesn't have access, or if something else went wrong. Can return None if user doesn't have access, or if something else went wrong.
cache: A ModelDataCache cache: A FieldDataCache
""" """
if not user.is_authenticated(): if not user.is_authenticated():
return (None, None) return (None, None)
...@@ -371,14 +371,14 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca ...@@ -371,14 +371,14 @@ def get_score(course_id, user, problem_descriptor, module_creator, model_data_ca
return (None, None) return (None, None)
# Create a fake KeyValueStore key to pull out the StudentModule # Create a fake KeyValueStore key to pull out the StudentModule
key = LmsKeyValueStore.Key( key = DjangoKeyValueStore.Key(
Scope.user_state, Scope.user_state,
user.id, user.id,
problem_descriptor.location, problem_descriptor.location,
None None
) )
student_module = model_data_cache.find(key) student_module = field_data_cache.find(key)
if student_module is not None and student_module.max_grade is not None: if student_module is not None and student_module.max_grade is not None:
correct = student_module.grade if student_module.grade is not None else 0 correct = student_module.grade if student_module.grade is not None else 0
......
...@@ -11,7 +11,7 @@ from django.contrib.auth.models import User ...@@ -11,7 +11,7 @@ from django.contrib.auth.models import User
import xmodule import xmodule
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from courseware.model_data import ModelDataCache from courseware.model_data import FieldDataCache
from courseware.module_render import get_module from courseware.module_render import get_module
...@@ -81,7 +81,7 @@ class Command(BaseCommand): ...@@ -81,7 +81,7 @@ class Command(BaseCommand):
# TODO (cpennington): Get coursename in a legitimate way # TODO (cpennington): Get coursename in a legitimate way
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012' course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
student_module_cache = ModelDataCache.cache_for_descriptor_descendents( student_module_cache = FieldDataCache.cache_for_descriptor_descendents(
course_id, course_id,
sample_user, modulestore().get_item(course_location)) sample_user, modulestore().get_item(course_location))
course = get_module(sample_user, None, course_location, student_module_cache) course = get_module(sample_user, None, course_location, student_module_cache)
......
...@@ -102,40 +102,9 @@ class StudentModuleHistory(models.Model): ...@@ -102,40 +102,9 @@ class StudentModuleHistory(models.Model):
history_entry.save() history_entry.save()
class XModuleContentField(models.Model): class XModuleUserStateSummaryField(models.Model):
""" """
Stores data set in the Scope.content scope by an xmodule field Stores data set in the Scope.user_state_summary scope by an xmodule field
"""
class Meta:
unique_together = (('definition_id', 'field_name'),)
# The name of the field
field_name = models.CharField(max_length=64, db_index=True)
# The definition id for the module
definition_id = models.CharField(max_length=255, db_index=True)
# The value of the field. Defaults to None dumped as json
value = models.TextField(default='null')
created = models.DateTimeField(auto_now_add=True, db_index=True)
modified = models.DateTimeField(auto_now=True, db_index=True)
def __repr__(self):
return 'XModuleContentField<%r>' % ({
'field_name': self.field_name,
'definition_id': self.definition_id,
'value': self.value,
},)
def __unicode__(self):
return unicode(repr(self))
class XModuleSettingsField(models.Model):
"""
Stores data set in the Scope.settings scope by an xmodule field
""" """
class Meta: class Meta:
...@@ -144,17 +113,17 @@ class XModuleSettingsField(models.Model): ...@@ -144,17 +113,17 @@ class XModuleSettingsField(models.Model):
# The name of the field # The name of the field
field_name = models.CharField(max_length=64, db_index=True) field_name = models.CharField(max_length=64, db_index=True)
# The usage id for the module # The definition id for the module
usage_id = models.CharField(max_length=255, db_index=True) usage_id = models.CharField(max_length=255, db_index=True)
# The value of the field. Defaults to None, dumped as json # The value of the field. Defaults to None dumped as json
value = models.TextField(default='null') value = models.TextField(default='null')
created = models.DateTimeField(auto_now_add=True, db_index=True) created = models.DateTimeField(auto_now_add=True, db_index=True)
modified = models.DateTimeField(auto_now=True, db_index=True) modified = models.DateTimeField(auto_now=True, db_index=True)
def __repr__(self): def __repr__(self):
return 'XModuleSettingsField<%r>' % ({ return 'XModuleUserStateSummaryField<%r>' % ({
'field_name': self.field_name, 'field_name': self.field_name,
'usage_id': self.usage_id, 'usage_id': self.usage_id,
'value': self.value, 'value': self.value,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment