Commit a65771df by Don Mitchell

Implement edit info as mixin on base mongo

LMS-11183, LMS-11184
parent 85fa712f
...@@ -44,6 +44,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey ...@@ -44,6 +44,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from opaque_keys.edx.keys import UsageKey, CourseKey from opaque_keys.edx.keys import UsageKey, CourseKey
from xmodule.exceptions import HeartbeatFailure from xmodule.exceptions import HeartbeatFailure
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -137,7 +138,7 @@ class MongoKeyValueStore(InheritanceKeyValueStore): ...@@ -137,7 +138,7 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
return False return False
class CachingDescriptorSystem(MakoDescriptorSystem): class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
""" """
A system that has a cache of module json that it will use to load modules A system that has a cache of module json that it will use to load modules
from, with a backup of calling to the underlying modulestore for more data from, with a backup of calling to the underlying modulestore for more data
...@@ -233,25 +234,16 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -233,25 +234,16 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
metadata_to_inherit = self.cached_metadata.get(unicode(non_draft_loc), {}) metadata_to_inherit = self.cached_metadata.get(unicode(non_draft_loc), {})
inherit_metadata(module, metadata_to_inherit) inherit_metadata(module, metadata_to_inherit)
edit_info = json_data.get('edit_info') module._edit_info = json_data.get('edit_info')
# migrate published_by and published_on if edit_info isn't present # migrate published_by and published_on if edit_info isn't present
if not edit_info: if module._edit_info is None:
module.edited_by = module.edited_on = module.subtree_edited_on = \ module._edit_info = {}
module.subtree_edited_by = module.published_on = None
raw_metadata = json_data.get('metadata', {}) raw_metadata = json_data.get('metadata', {})
# published_on was previously stored as a list of time components instead of a datetime # published_on was previously stored as a list of time components instead of a datetime
if raw_metadata.get('published_date'): if raw_metadata.get('published_date'):
module.published_on = datetime(*raw_metadata.get('published_date')[0:6]).replace(tzinfo=UTC) module._edit_info['published_date'] = datetime(*raw_metadata.get('published_date')[0:6]).replace(tzinfo=UTC)
module.published_by = raw_metadata.get('published_by') module._edit_info['published_by'] = raw_metadata.get('published_by')
# otherwise restore the stored editing information
else:
module.edited_by = edit_info.get('edited_by')
module.edited_on = edit_info.get('edited_on')
module.subtree_edited_on = edit_info.get('subtree_edited_on')
module.subtree_edited_by = edit_info.get('subtree_edited_by')
module.published_on = edit_info.get('published_date')
module.published_by = edit_info.get('published_by')
# decache any computed pending field settings # decache any computed pending field settings
module.save() module.save()
...@@ -316,6 +308,42 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -316,6 +308,42 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
return json return json
def get_edited_by(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
return xblock._edit_info.get('edited_by')
def get_edited_on(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
return xblock._edit_info.get('edited_on')
def get_subtree_edited_by(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
return xblock._edit_info.get('subtree_edited_by')
def get_subtree_edited_on(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
return xblock._edit_info.get('subtree_edited_on')
def get_published_by(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
return xblock._edit_info.get('published_by')
def get_published_on(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
return xblock._edit_info.get('published_date')
# The only thing using this w/ wildcards is contentstore.mongo for asset retrieval # The only thing using this w/ wildcards is contentstore.mongo for asset retrieval
def location_to_query(location, wildcard=True, tag='i4x'): def location_to_query(location, wildcard=True, tag='i4x'):
...@@ -1153,15 +1181,20 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -1153,15 +1181,20 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
payload = { payload = {
'definition.data': definition_data, 'definition.data': definition_data,
'metadata': self._serialize_scope(xblock, Scope.settings), 'metadata': self._serialize_scope(xblock, Scope.settings),
'edit_info.edited_on': now, 'edit_info': {
'edit_info.edited_by': user_id, 'edited_on': now,
'edit_info.subtree_edited_on': now, 'edited_by': user_id,
'edit_info.subtree_edited_by': user_id, 'subtree_edited_on': now,
'subtree_edited_by': user_id,
}
} }
if isPublish: if isPublish:
payload['edit_info.published_date'] = now payload['edit_info']['published_date'] = now
payload['edit_info.published_by'] = user_id payload['edit_info']['published_by'] = user_id
elif 'published_date' in getattr(xblock, '_edit_info', {}):
payload['edit_info']['published_date'] = xblock._edit_info['published_date']
payload['edit_info']['published_by'] = xblock._edit_info['published_by']
if xblock.has_children: if xblock.has_children:
children = self._serialize_scope(xblock, Scope.children) children = self._serialize_scope(xblock, Scope.children)
...@@ -1181,17 +1214,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -1181,17 +1214,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
self._update_ancestors(xblock.scope_ids.usage_id, ancestor_payload) self._update_ancestors(xblock.scope_ids.usage_id, ancestor_payload)
# update the edit info of the instantiated xblock # update the edit info of the instantiated xblock
xblock.edited_on = now xblock._edit_info = payload['edit_info']
xblock.edited_by = user_id
xblock.subtree_edited_on = now
xblock.subtree_edited_by = user_id
if not hasattr(xblock, 'published_on'):
xblock.published_on = None
if not hasattr(xblock, 'published_by'):
xblock.published_by = None
if isPublish:
xblock.published_on = now
xblock.published_by = user_id
# recompute (and update) the metadata inheritance tree which is cached # recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(xblock.scope_ids.usage_id.course_key, xblock.runtime) self.refresh_cached_metadata_inheritance_tree(xblock.scope_ids.usage_id.course_key, xblock.runtime)
......
...@@ -276,7 +276,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -276,7 +276,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
for child in json_data.get('fields', {}).get('children', []): for child in json_data.get('fields', {}).get('children', []):
child_data = self.get_module_data(child, course_key) child_data = self.get_module_data(child, course_key)
if '_subtree_edited_on' not in json_data.setdefault('edit_info', {}): if '_subtree_edited_on' not in json_data.setdefault('edit_info', {}):
self._compute_subtree_edited_internal(child, child_data) self._compute_subtree_edited_internal(child, child_data, course_key)
if child_data['edit_info']['_subtree_edited_on'] > max_date: if child_data['edit_info']['_subtree_edited_on'] > max_date:
max_date = child_data['edit_info']['_subtree_edited_on'] max_date = child_data['edit_info']['_subtree_edited_on']
max_by = child_data['edit_info']['_subtree_edited_by'] max_by = child_data['edit_info']['_subtree_edited_by']
......
...@@ -393,5 +393,6 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -393,5 +393,6 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
Get the published branch and find when it was published if it was. Cache the results in the xblock Get the published branch and find when it was published if it was. Cache the results in the xblock
""" """
published_block = self._get_head(xblock, ModuleStoreEnum.BranchName.published) published_block = self._get_head(xblock, ModuleStoreEnum.BranchName.published)
setattr(xblock, '_published_by', published_block['edit_info']['edited_by']) if published_block is not None:
setattr(xblock, '_published_on', published_block['edit_info']['edited_on']) setattr(xblock, '_published_by', published_block['edit_info']['edited_by'])
setattr(xblock, '_published_on', published_block['edit_info']['edited_on'])
...@@ -999,7 +999,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -999,7 +999,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(self.user_id, block.edited_by) self.assertEqual(self.user_id, block.edited_by)
self.assertGreater(datetime.datetime.now(UTC), block.edited_on) self.assertGreater(datetime.datetime.now(UTC), block.edited_on)
@ddt.data('draft') @ddt.data('draft', 'split')
def test_create_item_populates_subtree_edited_info(self, default_ms): def test_create_item_populates_subtree_edited_info(self, default_ms):
self.initdb(default_ms) self.initdb(default_ms)
block = self.store.create_item( block = self.store.create_item(
...@@ -1124,6 +1124,147 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -1124,6 +1124,147 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertTrue(self.store.has_published_version(item)) self.assertTrue(self.store.has_published_version(item))
@ddt.data('draft', 'split') @ddt.data('draft', 'split')
def test_update_edit_info_ancestors(self, default_ms):
"""
Tests that edited_on, edited_by, subtree_edited_on, and subtree_edited_by are set correctly during update
"""
self.initdb(default_ms)
test_course = self.store.create_course('testx', 'GreekHero', 'test_run', self.user_id)
def check_node(location_key, after, before, edited_by, subtree_after, subtree_before, subtree_by):
"""
Checks that the node given by location_key matches the given edit_info constraints.
"""
node = self.store.get_item(location_key)
if after:
self.assertLess(after, node.edited_on)
self.assertLess(node.edited_on, before)
self.assertEqual(node.edited_by, edited_by)
if subtree_after:
self.assertLess(subtree_after, node.subtree_edited_on)
self.assertLess(node.subtree_edited_on, subtree_before)
self.assertEqual(node.subtree_edited_by, subtree_by)
# Create a dummy vertical & html to test against
component = self.store.create_child(
self.user_id,
test_course.location,
'vertical',
block_id='test_vertical'
)
child = self.store.create_child(
self.user_id,
component.location,
'html',
block_id='test_html'
)
sibling = self.store.create_child(
self.user_id,
component.location,
'html',
block_id='test_html_no_change'
)
after_create = datetime.datetime.now(UTC)
# Verify that all nodes were last edited in the past by create_user
[
check_node(block.location, None, after_create, self.user_id, None, after_create, self.user_id)
for block in [component, child, sibling]
]
# Change the component, then check that there now are changes
component.display_name = 'Changed Display Name'
editing_user = self.user_id - 2
component = self.store.update_item(component, editing_user)
after_edit = datetime.datetime.now(UTC)
check_node(component.location, after_create, after_edit, editing_user, after_create, after_edit, editing_user)
# but child didn't change
check_node(child.location, None, after_create, self.user_id, None, after_create, self.user_id)
# Change the child
child = self.store.get_item(child.location)
child.display_name = 'Changed Display Name'
self.store.update_item(child, user_id=editing_user)
after_edit = datetime.datetime.now(UTC)
# Verify that child was last edited between after_create and after_edit by edit_user
check_node(child.location, after_create, after_edit, editing_user, after_create, after_edit, editing_user)
# Verify that ancestors edit info is unchanged, but their subtree edit info matches child
check_node(test_course.location, None, after_create, self.user_id, after_create, after_edit, editing_user)
# Verify that others have unchanged edit info
check_node(sibling.location, None, after_create, self.user_id, None, after_create, self.user_id)
@ddt.data('draft', 'split')
def test_update_edit_info(self, default_ms):
"""
Tests that edited_on and edited_by are set correctly during an update
"""
self.initdb(default_ms)
test_course = self.store.create_course('testx', 'GreekHero', 'test_run', self.user_id)
# Create a dummy component to test against
component = self.store.create_child(
self.user_id,
test_course.location,
'vertical',
)
# Store the current edit time and verify that user created the component
self.assertEqual(component.edited_by, self.user_id)
old_edited_on = component.edited_on
edit_user = self.user_id - 2
# Change the component
component.display_name = 'Changed'
self.store.update_item(component, edit_user)
updated_component = self.store.get_item(component.location)
# Verify the ordering of edit times and that dummy_user made the edit
self.assertLess(old_edited_on, updated_component.edited_on)
self.assertEqual(updated_component.edited_by, edit_user)
@ddt.data('draft', 'split')
def test_update_published_info(self, default_ms):
"""
Tests that published_on and published_by are set correctly
"""
self.initdb(default_ms)
test_course = self.store.create_course('testx', 'GreekHero', 'test_run', self.user_id)
publish_user = 456
# Create a dummy component to test against
component = self.store.create_child(
self.user_id,
test_course.location,
'vertical',
)
# Store the current time, then publish
old_time = datetime.datetime.now(UTC)
self.store.publish(component.location, publish_user)
updated_component = self.store.get_item(component.location)
# Verify the time order and that publish_user caused publication
self.assertLessEqual(old_time, updated_component.published_on)
self.assertEqual(updated_component.published_by, publish_user)
# Verify that changing the item doesn't unset the published info
updated_component.display_name = 'changed'
self.store.update_item(updated_component, self.user_id)
updated_component = self.store.get_item(updated_component.location)
self.assertLessEqual(old_time, updated_component.published_on)
self.assertEqual(updated_component.published_by, publish_user)
@ddt.data('draft', 'split')
def test_auto_publish(self, default_ms): def test_auto_publish(self, default_ms):
""" """
Test that the correct things have been published automatically Test that the correct things have been published automatically
......
...@@ -37,6 +37,7 @@ from git.test.lib.asserts import assert_not_none ...@@ -37,6 +37,7 @@ from git.test.lib.asserts import assert_not_none
from xmodule.x_module import XModuleMixin from xmodule.x_module import XModuleMixin
from xmodule.modulestore.mongo.base import as_draft from xmodule.modulestore.mongo.base import as_draft
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.modulestore.edit_info import EditInfoMixin
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -105,7 +106,9 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -105,7 +106,9 @@ class TestMongoModuleStore(unittest.TestCase):
content_store, content_store,
doc_store_config, FS_ROOT, RENDER_TEMPLATE, doc_store_config, FS_ROOT, RENDER_TEMPLATE,
default_class=DEFAULT_CLASS, default_class=DEFAULT_CLASS,
branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
xblock_mixins=(EditInfoMixin,)
) )
import_from_xml( import_from_xml(
draft_store, draft_store,
...@@ -706,106 +709,6 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -706,106 +709,6 @@ class TestMongoModuleStore(unittest.TestCase):
self.assertTrue(self._has_changes(parent_location)) self.assertTrue(self._has_changes(parent_location))
self.assertTrue(self._has_changes(child_location)) self.assertTrue(self._has_changes(child_location))
def test_update_edit_info_ancestors(self):
"""
Tests that edited_on, edited_by, subtree_edited_on, and subtree_edited_by are set correctly during update
"""
create_user = 123
edit_user = 456
locations =self._create_test_tree('update_edit_info_ancestors', create_user)
def check_node(location_key, after, before, edited_by, subtree_after, subtree_before, subtree_by):
"""
Checks that the node given by location_key matches the given edit_info constraints.
"""
node = self.draft_store.get_item(locations[location_key])
if after:
self.assertLess(after, node.edited_on)
self.assertLess(node.edited_on, before)
self.assertEqual(node.edited_by, edited_by)
if subtree_after:
self.assertLess(subtree_after, node.subtree_edited_on)
self.assertLess(node.subtree_edited_on, subtree_before)
self.assertEqual(node.subtree_edited_by, subtree_by)
after_create = datetime.now(UTC)
# Verify that all nodes were last edited in the past by create_user
for key in locations:
check_node(key, None, after_create, create_user, None, after_create, create_user)
# Change the child
child = self.draft_store.get_item(locations['child'])
child.display_name = 'Changed Display Name'
self.draft_store.update_item(child, user_id=edit_user)
after_edit = datetime.now(UTC)
ancestors = ['parent', 'grandparent']
others = ['child_sibling', 'parent_sibling']
# Verify that child was last edited between after_create and after_edit by edit_user
check_node('child', after_create, after_edit, edit_user, after_create, after_edit, edit_user)
# Verify that ancestors edit info is unchanged, but their subtree edit info matches child
for key in ancestors:
check_node(key, None, after_create, create_user, after_create, after_edit, edit_user)
# Verify that others have unchanged edit info
for key in others:
check_node(key, None, after_create, create_user, None, after_create, create_user)
def test_update_edit_info(self):
"""
Tests that edited_on and edited_by are set correctly during an update
"""
location = Location('edX', 'toy', '2012_Fall', 'html', 'test_html')
# Create a dummy component to test against
self.draft_store.create_item(
self.dummy_user,
location.course_key,
location.block_type,
block_id=location.block_id
)
# Store the current edit time and verify that dummy_user created the component
component = self.draft_store.get_item(location)
self.assertEqual(component.edited_by, self.dummy_user)
old_edited_on = component.edited_on
# Change the component
component.display_name = component.display_name + ' Changed'
self.draft_store.update_item(component, self.dummy_user)
updated_component = self.draft_store.get_item(location)
# Verify the ordering of edit times and that dummy_user made the edit
self.assertLess(old_edited_on, updated_component.edited_on)
self.assertEqual(updated_component.edited_by, self.dummy_user)
def test_update_published_info(self):
"""
Tests that published_on and published_by are set correctly
"""
location = Location('edX', 'toy', '2012_Fall', 'html', 'test_html')
create_user = 123
publish_user = 456
# Create a dummy component to test against
self.draft_store.create_item(
create_user,
location.course_key,
location.block_type,
block_id=location.block_id
)
# Store the current time, then publish
old_time = datetime.now(UTC)
self.draft_store.publish(location, publish_user)
updated_component = self.draft_store.get_item(location)
# Verify the time order and that publish_user caused publication
self.assertLessEqual(old_time, updated_component.published_on)
self.assertEqual(updated_component.published_by, publish_user)
def test_migrate_published_info(self): def test_migrate_published_info(self):
""" """
Tests that blocks that were storing published_date and published_by through CMSBlockMixin are loaded correctly Tests that blocks that were storing published_date and published_by through CMSBlockMixin are loaded correctly
......
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