Commit b7680f31 by Calen Pennington

Fix tests except for conditional module and open ended grading

parent 0d83fefe
...@@ -6,6 +6,7 @@ from django.conf import settings ...@@ -6,6 +6,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from path import path from path import path
from tempdir import mkdtemp_clean from tempdir import mkdtemp_clean
from datetime import timedelta
import json import json
from fs.osfs import OSFS from fs.osfs import OSFS
import copy import copy
...@@ -27,6 +28,7 @@ from xmodule.contentstore.django import contentstore ...@@ -27,6 +28,7 @@ from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.inheritance import own_metadata
from xmodule.templates import update_templates from xmodule.templates import update_templates
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
...@@ -218,7 +220,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -218,7 +220,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# compare what's on disk compared to what we have in our course # compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json','r') as grading_policy: with fs.open('grading_policy.json','r') as grading_policy:
on_disk = loads(grading_policy.read()) on_disk = loads(grading_policy.read())
self.assertEqual(on_disk, course.definition['data']['grading_policy']) self.assertEqual(on_disk, course.grading_policy)
#check for policy.json #check for policy.json
self.assertTrue(fs.exists('policy.json')) self.assertTrue(fs.exists('policy.json'))
...@@ -227,7 +229,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -227,7 +229,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
with fs.open('policy.json','r') as course_policy: with fs.open('policy.json','r') as course_policy:
on_disk = loads(course_policy.read()) on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk) self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata) self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
# remove old course # remove old course
delete_course(ms, cs, location) delete_course(ms, cs, location)
...@@ -444,8 +446,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -444,8 +446,7 @@ 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.assertIn('xqa_key', vertical.metadata) self.assertEqual(course.lms.xqa_key, vertical.lms.xqa_key)
self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
self.assertGreater(len(verticals), 0) self.assertGreater(len(verticals), 0)
...@@ -455,31 +456,28 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -455,31 +456,28 @@ class ContentStoreTest(ModuleStoreTestCase):
# crate a new module and add it as a child to a vertical # crate a new module and add it as a child to a vertical
ms.clone_item(source_template_location, new_component_location) ms.clone_item(source_template_location, new_component_location)
parent = verticals[0] parent = verticals[0]
ms.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()]) ms.update_children(parent.location, parent.children + [new_component_location.url()])
# flush the cache # flush the cache
ms.get_cached_metadata_inheritance_tree(new_component_location, -1) ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location) new_module = ms.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.assertIn('graceperiod', new_module.metadata) self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
self.assertEqual(parent.metadata['graceperiod'], new_module.metadata['graceperiod']) self.assertEqual(course.lms.xqa_key, new_module.lms.xqa_key)
self.assertEqual(course.metadata['xqa_key'], new_module.metadata['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.metadata['graceperiod'] = '1 day' new_module.lms.graceperiod = timedelta(1)
ms.update_metadata(new_module.location, new_module.metadata) ms.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch # flush the cache and refetch
ms.get_cached_metadata_inheritance_tree(new_component_location, -1) ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location) new_module = ms.get_item(new_component_location)
self.assertIn('graceperiod', new_module.metadata) self.assertEqual(timedelta(1), new_module.lms.graceperiod)
self.assertEqual('1 day', new_module.metadata['graceperiod'])
class TemplateTestCase(ModuleStoreTestCase): class TemplateTestCase(ModuleStoreTestCase):
......
...@@ -287,31 +287,31 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -287,31 +287,31 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_update_from_json(self): def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course_location, test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 1, { "advertised_start" : "start A",
"b_a_c_h" : { "c" : "test" }, "testcenter_info" : { "c" : "test" },
"test_text" : "a text string"}) "days_early_for_beta" : 2})
self.update_check(test_model) self.update_check(test_model)
# try fresh fetch to ensure persistence # try fresh fetch to ensure persistence
test_model = CourseMetadata.fetch(self.course_location) test_model = CourseMetadata.fetch(self.course_location)
self.update_check(test_model) self.update_check(test_model)
# now change some of the existing metadata # now change some of the existing metadata
test_model = CourseMetadata.update_from_json(self.course_location, test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 2, { "advertised_start" : "start B",
"display_name" : "jolly roger"}) "display_name" : "jolly roger"})
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value") self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
self.assertIn('a', test_model, 'Missing revised a metadata field') self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
self.assertEqual(test_model['a'], 2, "a not expected value") self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value")
def update_check(self, test_model): def update_check(self, test_model):
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('a', test_model, 'Missing new a metadata field') self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
self.assertEqual(test_model['a'], 1, "a not expected value") self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field') self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value") self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
self.assertIn('test_text', test_model, 'Missing test_text metadata field') self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
self.assertEqual(test_model['test_text'], "a text string", "test_text not expected value") self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
def test_delete_key(self): def test_delete_key(self):
...@@ -322,5 +322,5 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -322,5 +322,5 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(test_model['display_name'], 'Testing', "not expected value") self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field') self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness # check for deletion effectiveness
self.assertNotIn('showanswer', test_model, 'showanswer field still in') self.assertEqual('closed', test_model['showanswer'], 'showanswer field still in')
self.assertNotIn('xqa_key', test_model, 'xqa_key field still in') self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
...@@ -464,6 +464,9 @@ class SessionKeyValueStore(KeyValueStore): ...@@ -464,6 +464,9 @@ class SessionKeyValueStore(KeyValueStore):
except (KeyError, InvalidScopeError): except (KeyError, InvalidScopeError):
del self._session[tuple(key)] del self._session[tuple(key)]
def has(self, key):
return key in self._model_data or key in self._session
def preview_module_system(request, preview_id, descriptor): def preview_module_system(request, preview_id, descriptor):
""" """
...@@ -662,7 +665,7 @@ def save_item(request): ...@@ -662,7 +665,7 @@ def save_item(request):
# 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
store.update_metadata(item_location, existing_item._model_data._kvs._metadata) store.update_metadata(item_location, own_metadat(existing_item))
return HttpResponse() return HttpResponse()
......
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata
class CourseMetadata(object): class CourseMetadata(object):
...@@ -10,7 +11,7 @@ class CourseMetadata(object): ...@@ -10,7 +11,7 @@ class CourseMetadata(object):
''' '''
# __new_advanced_key__ is used by client not server; so, could argue against it being here # __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__'] FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
@classmethod @classmethod
def fetch(cls, course_location): def fetch(cls, course_location):
""" """
...@@ -18,53 +19,60 @@ class CourseMetadata(object): ...@@ -18,53 +19,60 @@ class CourseMetadata(object):
""" """
if not isinstance(course_location, Location): if not isinstance(course_location, Location):
course_location = Location(course_location) course_location = Location(course_location)
course = {} course = {}
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
for k, v in descriptor.metadata.iteritems(): for field in descriptor.fields + descriptor.lms.fields:
if k not in cls.FILTERED_LIST: if field.name not in cls.FILTERED_LIST:
course[k] = v course[field.name] = field.read_from(descriptor)
return course return course
@classmethod @classmethod
def update_from_json(cls, course_location, jsondict): def update_from_json(cls, course_location, jsondict):
""" """
Decode the json into CourseMetadata and save any changed attrs to the db. Decode the json into CourseMetadata and save any changed attrs to the db.
Ensures none of the fields are in the blacklist. Ensures none of the fields are in the blacklist.
""" """
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False dirty = False
for k, v in jsondict.iteritems(): for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload? # should it be an error if one of the filtered list items is in the payload?
if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v): if k in cls.FILTERED_LIST:
continue
if hasattr(descriptor, k) and getattr(descriptor, k) != v:
dirty = True
setattr(descriptor, k, v)
elif hasattr(descriptor.lms, k) and getattr(descriptor.lms, k) != k:
dirty = True dirty = True
descriptor.metadata[k] = v setattr(descriptor.lms, k, v)
if dirty: if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata) get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm # Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly # it persisted correctly
return cls.fetch(course_location) return cls.fetch(course_location)
@classmethod @classmethod
def delete_key(cls, course_location, payload): def delete_key(cls, course_location, payload):
''' '''
Remove the given metadata key(s) from the course. payload can be a single key or [key..] Remove the given metadata key(s) from the course. payload can be a single key or [key..]
''' '''
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']: for key in payload['deleteKeys']:
if key in descriptor.metadata: if hasattr(descriptor, key):
del descriptor.metadata[key] delattr(descriptor, key)
elif hasattr(descriptor.lms, key):
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata) delattr(descriptor.lms, key)
get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor))
return cls.fetch(course_location) return cls.fetch(course_location)
\ No newline at end of file
...@@ -50,7 +50,7 @@ class StringOrDate(Date): ...@@ -50,7 +50,7 @@ class StringOrDate(Date):
try: try:
return time.strftime(self.time_format, value) return time.strftime(self.time_format, value)
except ValueError: except (ValueError, TypeError):
return value return value
...@@ -449,8 +449,10 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -449,8 +449,10 @@ class CourseDescriptor(SequenceDescriptor):
specified. Returns specified list even if is_cohorted and/or auto_cohort are specified. Returns specified list even if is_cohorted and/or auto_cohort are
false. false.
""" """
return self.metadata.get("cohort_config", {}).get( if self.cohort_config is None:
"auto_cohort_groups", []) return []
else:
return self.cohort_config.get("auto_cohort_groups", [])
@property @property
......
...@@ -45,9 +45,9 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -45,9 +45,9 @@ class MakoModuleDescriptor(XModuleDescriptor):
@property @property
def editable_metadata_fields(self): def editable_metadata_fields(self):
fields = {} fields = {}
for field, value in own_metadata(self): for field, value in own_metadata(self).items():
if field.name in self.system_metadata_fields: if field in self.system_metadata_fields:
continue continue
fields[field.name] = value fields[field] = value
return fields return fields
...@@ -5,9 +5,6 @@ INHERITABLE_METADATA = ( ...@@ -5,9 +5,6 @@ INHERITABLE_METADATA = (
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize', 'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
# TODO (ichuang): used for Fall 2012 xqa server access # TODO (ichuang): used for Fall 2012 xqa server access
'xqa_key', 'xqa_key',
# TODO: This is used by the XMLModuleStore to provide for locations for
# static files, and will need to be removed when that code is removed
'data_dir'
# How many days early to show a course element to beta testers (float) # 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 # intended to be set per-course, but can be overridden in for specific
# elements. Can be a float. # elements. Can be a float.
...@@ -33,13 +30,13 @@ def inherit_metadata(descriptor, model_data): ...@@ -33,13 +30,13 @@ def inherit_metadata(descriptor, model_data):
be inherited be inherited
""" """
if not hasattr(descriptor, '_inherited_metadata'): if not hasattr(descriptor, '_inherited_metadata'):
setattr(descriptor, '_inherited_metadata', set()) setattr(descriptor, '_inherited_metadata', {})
# Set all inheritable metadata from kwargs that are # Set all inheritable metadata from kwargs that are
# in self.inheritable_metadata and aren't already set in metadata # in self.inheritable_metadata and aren't already set in metadata
for attr in INHERITABLE_METADATA: for attr in INHERITABLE_METADATA:
if attr not in descriptor._model_data and attr in model_data: if attr not in descriptor._model_data and attr in model_data:
descriptor._inherited_metadata.add(attr) descriptor._inherited_metadata[attr] = model_data[attr]
descriptor._model_data[attr] = model_data[attr] descriptor._model_data[attr] = model_data[attr]
...@@ -52,15 +49,19 @@ def own_metadata(module): ...@@ -52,15 +49,19 @@ def own_metadata(module):
metadata = {} metadata = {}
for field in module.fields + module.lms.fields: for field in module.fields + module.lms.fields:
# Only save metadata that wasn't inherited # Only save metadata that wasn't inherited
if (field.scope == Scope.settings and if field.scope != Scope.settings:
field.name not in inherited_metadata and continue
field.name in module._model_data):
try: if field.name in inherited_metadata and module._model_data[field.name] == inherited_metadata[field.name]:
metadata[field.name] = field.read_from(module) continue
except KeyError:
# Ignore any missing keys in _model_data
pass
if field.name not in module._model_data:
continue
try:
metadata[field.name] = module._model_data[field.name]
except KeyError:
# Ignore any missing keys in _model_data
pass
return metadata return metadata
...@@ -22,7 +22,7 @@ from . import ModuleStoreBase, Location ...@@ -22,7 +22,7 @@ from . import ModuleStoreBase, Location
from .draft import DraftModuleStore from .draft import DraftModuleStore
from .exceptions import (ItemNotFoundError, from .exceptions import (ItemNotFoundError,
DuplicateItemError) DuplicateItemError)
from .inheritance import own_metadata, INHERITABLE_METADATA from .inheritance import own_metadata, INHERITABLE_METADATA, inherit_metadata
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -84,6 +84,18 @@ class MongoKeyValueStore(KeyValueStore): ...@@ -84,6 +84,18 @@ class MongoKeyValueStore(KeyValueStore):
else: else:
raise InvalidScopeError(key.scope) raise InvalidScopeError(key.scope)
def has(self, key):
if key.scope in (Scope.children, Scope.parent):
return True
elif key.scope == Scope.settings:
return key.field_name in self._metadata
elif key.scope == Scope.content:
if key.field_name == 'data' and not isinstance(self._data, dict):
return True
else:
return key.field_name in self._data
else:
raise InvalidScopeError(key.scope)
MongoUsage = namedtuple('MongoUsage', 'id, def_id') MongoUsage = namedtuple('MongoUsage', 'id, def_id')
...@@ -146,7 +158,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -146,7 +158,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
module = class_(self, location, model_data) module = class_(self, location, model_data)
if self.metadata_inheritance_tree is not None: if self.metadata_inheritance_tree is not None:
metadata_to_inherit = self.metadata_inheritance_tree.get('parent_metadata', {}).get(location.url(), {}) metadata_to_inherit = self.metadata_inheritance_tree.get('parent_metadata', {}).get(location.url(), {})
module.inherit_metadata(metadata_to_inherit) inherit_metadata(module, metadata_to_inherit)
return module return module
except: except:
log.debug("Failed to load descriptor", exc_info=True) log.debug("Failed to load descriptor", exc_info=True)
......
import logging import logging
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from fs.osfs import OSFS from fs.osfs import OSFS
from json import dumps from json import dumps
...@@ -31,14 +32,12 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d ...@@ -31,14 +32,12 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
# export the grading policy # export the grading policy
policies_dir = export_fs.makeopendir('policies') policies_dir = export_fs.makeopendir('policies')
course_run_policy_dir = policies_dir.makeopendir(course.location.name) course_run_policy_dir = policies_dir.makeopendir(course.location.name)
if 'grading_policy' in course.definition['data']: with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy: grading_policy.write(dumps(course.grading_policy))
grading_policy.write(dumps(course.grading_policy))
# export all of the course metadata in policy.json # export all of the course metadata in policy.json
with course_run_policy_dir.open('policy.json', 'w') as course_policy: with course_run_policy_dir.open('policy.json', 'w') as course_policy:
policy = {} policy = {'course/' + course.location.name: own_metadata(course)}
policy = {'course/' + course.location.name: course.metadata}
course_policy.write(dumps(policy)) course_policy.write(dumps(policy))
......
...@@ -98,6 +98,8 @@ class CapaFactory(object): ...@@ -98,6 +98,8 @@ class CapaFactory(object):
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...
module.get_score = lambda: {'score': 1, 'total': 1} module.get_score = lambda: {'score': 1, 'total': 1}
else:
module.get_score = lambda: {'score': 0, 'total': 1}
return module return module
......
...@@ -336,6 +336,25 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -336,6 +336,25 @@ class LmsKeyValueStore(KeyValueStore):
else: else:
field_object.delete() field_object.delete()
def has(self, key):
if key.field_name in self._descriptor_model_data:
return key.field_name in self._descriptor_model_data
if key.scope == Scope.parent:
return True
if key.scope not in self._allowed_scopes:
raise InvalidScopeError(key.scope)
field_object = self._model_data_cache.find(key)
if field_object is None:
return False
if key.scope == Scope.student_state:
return key.field_name in json.loads(field_object.state)
else:
return True
LmsUsage = namedtuple('LmsUsage', 'id, def_id') LmsUsage = namedtuple('LmsUsage', 'id, def_id')
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
# XBlock: # XBlock:
# Might change frequently, so put it in local-requirements.txt, # Might change frequently, so put it in local-requirements.txt,
# but conceptually is an external package, so it is in a separate repo. # but conceptually is an external package, so it is in a separate repo.
-e git+ssh://git@github.com/MITx/xmodule-debugger@8f82a3b7fc#egg=XBlock -e git+ssh://git@github.com/MITx/xmodule-debugger@e3c4bc#egg=XBlock
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