Commit 96bc5cc8 by Don Mitchell

Merge pull request #5001 from edx/split/edit_info

Split/edit info
parents c5fc9b2f a65771df
...@@ -669,7 +669,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -669,7 +669,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"category": xblock.category, "category": xblock.category,
"edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None,
"published": published, "published": published,
"published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None, "published_on": get_default_time_display(xblock.published_on) if xblock.published_on else None,
"studio_url": xblock_studio_url(xblock, parent_xblock), "studio_url": xblock_studio_url(xblock, parent_xblock),
"released_to_students": datetime.now(UTC) > xblock.start, "released_to_students": datetime.now(UTC) > xblock.start,
"release_date": release_date, "release_date": release_date,
......
...@@ -38,6 +38,7 @@ from warnings import simplefilter ...@@ -38,6 +38,7 @@ from warnings import simplefilter
from lms.lib.xblock.mixin import LmsBlockMixin from lms.lib.xblock.mixin import LmsBlockMixin
from dealer.git import git from dealer.git import git
from xmodule.modulestore.edit_info import EditInfoMixin
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
...@@ -254,7 +255,7 @@ from xmodule.x_module import XModuleMixin ...@@ -254,7 +255,7 @@ from xmodule.x_module import XModuleMixin
# This should be moved into an XBlock Runtime/Application object # This should be moved into an XBlock Runtime/Application object
# once the responsibility of XBlock creation is moved out of modulestore - cpennington # once the responsibility of XBlock creation is moved out of modulestore - cpennington
XBLOCK_MIXINS = (LmsBlockMixin, InheritanceMixin, XModuleMixin) XBLOCK_MIXINS = (LmsBlockMixin, InheritanceMixin, XModuleMixin, EditInfoMixin)
# Allow any XBlock in Studio # Allow any XBlock in Studio
# You should also enable the ALLOW_ALL_ADVANCED_COMPONENTS feature flag, so that # You should also enable the ALLOW_ALL_ADVANCED_COMPONENTS feature flag, so that
......
"""
Access methods to get EditInfo for xblocks
"""
from xblock.fields import XBlockMixin
from abc import ABCMeta, abstractmethod
class EditInfoMixin(XBlockMixin):
"""
Provides the interfaces for getting the edit info from XBlocks
"""
@property
def edited_by(self):
"""
The user id of the last user to change this xblock content, children, or settings.
"""
return self.runtime.get_edited_by(self)
@property
def edited_on(self):
"""
The datetime of the last change to this xblock content, children, or settings.
"""
return self.runtime.get_edited_on(self)
@property
def subtree_edited_by(self):
"""
The user id of the last user to change content, children, or settings in this xblock's subtree
"""
return self.runtime.get_subtree_edited_by(self)
@property
def subtree_edited_on(self):
"""
The datetime of the last change content, children, or settings in this xblock's subtree
"""
return self.runtime.get_subtree_edited_on(self)
@property
def published_by(self):
"""
The user id of the last user to publish this specific xblock (or a previous version of it).
"""
return self.runtime.get_published_by(self)
@property
def published_on(self):
"""
The datetime of the last time this specific xblock was published.
"""
return self.runtime.get_published_on(self)
class EditInfoRuntimeMixin(object):
"""
An abstract mixin class for the functions which the :class: `EditInfoMixin` methods call on the runtime
"""
__metaclass__ = ABCMeta
@abstractmethod
def get_edited_by(self, xblock):
"""
The datetime of the last change to this xblock content, children, or settings.
"""
pass
@abstractmethod
def get_edited_on(self, xblock):
"""
The datetime of the last change to this xblock content, children, or settings.
"""
pass
@abstractmethod
def get_subtree_edited_by(self, xblock):
"""
The user id of the last user to change content, children, or settings in this xblock's subtree
"""
pass
@abstractmethod
def get_subtree_edited_on(self, xblock):
"""
The datetime of the last change content, children, or settings in this xblock's subtree
"""
pass
@abstractmethod
def get_published_by(self, xblock):
"""
The user id of the last user to publish this specific xblock (or a previous version of it).
"""
pass
@abstractmethod
def get_published_on(self, xblock):
"""
The datetime of the last time this specific xblock was published.
"""
pass
...@@ -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_date 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_date = None
raw_metadata = json_data.get('metadata', {}) raw_metadata = json_data.get('metadata', {})
# published_date 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_date = 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_date = 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_date'):
xblock.published_date = None
if not hasattr(xblock, 'published_by'):
xblock.published_by = None
if isPublish:
xblock.published_date = 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)
......
...@@ -12,10 +12,11 @@ import logging ...@@ -12,10 +12,11 @@ import logging
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.exceptions import InvalidVersionError from xmodule.exceptions import InvalidVersionError
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateItemError, DuplicateCourseError from xmodule.modulestore.exceptions import (
ItemNotFoundError, DuplicateItemError, DuplicateCourseError, InvalidBranchSetting
)
from xmodule.modulestore.mongo.base import ( from xmodule.modulestore.mongo.base import (
MongoModuleStore, MongoRevisionKey, as_draft, as_published, MongoModuleStore, MongoRevisionKey, as_draft, as_published, SORT_REVISION_FAVOR_DRAFT
SORT_REVISION_FAVOR_DRAFT
) )
from xmodule.modulestore.store_utilities import rewrite_nonportable_content_links from xmodule.modulestore.store_utilities import rewrite_nonportable_content_links
from xmodule.modulestore.draft_and_published import UnsupportedRevisionError, DIRECT_ONLY_CATEGORIES from xmodule.modulestore.draft_and_published import UnsupportedRevisionError, DIRECT_ONLY_CATEGORIES
......
...@@ -11,11 +11,12 @@ from ..exceptions import ItemNotFoundError ...@@ -11,11 +11,12 @@ from ..exceptions import ItemNotFoundError
from .split_mongo_kvs import SplitMongoKVS from .split_mongo_kvs import SplitMongoKVS
from fs.osfs import OSFS from fs.osfs import OSFS
from .definition_lazy_loader import DefinitionLazyLoader from .definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CachingDescriptorSystem(MakoDescriptorSystem): class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
""" """
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
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.
...@@ -89,6 +90,19 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -89,6 +90,19 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
run=course_info.get('run'), run=course_info.get('run'),
branch=course_info.get('branch'), branch=course_info.get('branch'),
) )
json_data = self.get_module_data(block_id, course_key)
class_ = self.load_block_type(json_data.get('category'))
new_item = self.xblock_from_json(class_, course_key, block_id, json_data, course_entry_override, **kwargs)
return new_item
def get_module_data(self, block_id, course_key):
"""
Get block from module_data adding it to module_data if it's not already there but is in the structure
Raises:
ItemNotFoundError if block is not in the structure
"""
json_data = self.module_data.get(block_id) json_data = self.module_data.get(block_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
...@@ -97,9 +111,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -97,9 +111,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
if json_data is None: if json_data is None:
raise ItemNotFoundError(block_id) raise ItemNotFoundError(block_id)
class_ = self.load_block_type(json_data.get('category')) return json_data
new_item = self.xblock_from_json(class_, course_key, block_id, json_data, course_entry_override, **kwargs)
return new_item
# xblock's runtime does not always pass enough contextual information to figure out # xblock's runtime does not always pass enough contextual information to figure out
# which named container (course x branch) or which parent is requesting an item. Because split allows # which named container (course x branch) or which parent is requesting an item. Because split allows
...@@ -181,12 +193,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -181,12 +193,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
) )
edit_info = json_data.get('edit_info', {}) edit_info = json_data.get('edit_info', {})
module.edited_by = edit_info.get('edited_by') module._edited_by = edit_info.get('edited_by')
module.edited_on = edit_info.get('edited_on') module._edited_on = edit_info.get('edited_on')
module.subtree_edited_by = None # TODO - addressed with LMS-11183
module.subtree_edited_on = None # TODO - addressed with LMS-11183
module.published_by = None # TODO - addressed with LMS-11184
module.published_date = None # TODO - addressed with LMS-11184
module.previous_version = edit_info.get('previous_version') module.previous_version = edit_info.get('previous_version')
module.update_version = edit_info.get('update_version') module.update_version = edit_info.get('update_version')
module.source_version = edit_info.get('source_version', None) module.source_version = edit_info.get('source_version', None)
...@@ -199,3 +207,79 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -199,3 +207,79 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
self.local_modules[block_locator] = module self.local_modules[block_locator] = module
return module return module
def get_edited_by(self, xblock):
"""
See :meth: cms.lib.xblock.runtime.EditInfoRuntimeMixin.get_edited_by
"""
return xblock._edited_by
def get_edited_on(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
return xblock._edited_on
def get_subtree_edited_by(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
if not hasattr(xblock, '_subtree_edited_by'):
json_data = self.module_data[xblock.location.block_id]
if '_subtree_edited_by' not in json_data.setdefault('edit_info', {}):
self._compute_subtree_edited_internal(
xblock.location.block_id, json_data, xblock.location.course_key
)
setattr(xblock, '_subtree_edited_by', json_data['edit_info']['_subtree_edited_by'])
return getattr(xblock, '_subtree_edited_by')
def get_subtree_edited_on(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
if not hasattr(xblock, '_subtree_edited_on'):
json_data = self.module_data[xblock.location.block_id]
if '_subtree_edited_on' not in json_data.setdefault('edit_info', {}):
self._compute_subtree_edited_internal(
xblock.location.block_id, json_data, xblock.location.course_key
)
setattr(xblock, '_subtree_edited_on', json_data['edit_info']['_subtree_edited_on'])
return getattr(xblock, '_subtree_edited_on')
def get_published_by(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
if not hasattr(xblock, '_published_by'):
self.modulestore.compute_published_info_internal(xblock)
return getattr(xblock, '_published_by', None)
def get_published_on(self, xblock):
"""
See :class: cms.lib.xblock.runtime.EditInfoRuntimeMixin
"""
if not hasattr(xblock, '_published_on'):
self.modulestore.compute_published_info_internal(xblock)
return getattr(xblock, '_published_on', None)
def _compute_subtree_edited_internal(self, block_id, json_data, course_key):
"""
Recurse the subtree finding the max edited_on date and its concomitant edited_by. Cache it
"""
max_date = json_data['edit_info']['edited_on']
max_by = json_data['edit_info']['edited_by']
for child in json_data.get('fields', {}).get('children', []):
child_data = self.get_module_data(child, course_key)
if '_subtree_edited_on' not in json_data.setdefault('edit_info', {}):
self._compute_subtree_edited_internal(child, child_data, course_key)
if child_data['edit_info']['_subtree_edited_on'] > max_date:
max_date = child_data['edit_info']['_subtree_edited_on']
max_by = child_data['edit_info']['_subtree_edited_by']
json_data['edit_info']['_subtree_edited_on'] = max_date
json_data['edit_info']['_subtree_edited_by'] = max_by
...@@ -387,3 +387,12 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -387,3 +387,12 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
return self._update_item_from_fields( return self._update_item_from_fields(
user_id, course_key, block_type, block_id, partitioned_fields, None, allow_not_found=True, force=True user_id, course_key, block_type, block_id, partitioned_fields, None, allow_not_found=True, force=True
) )
def compute_published_info_internal(self, 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)
if published_block is not None:
setattr(xblock, '_published_by', published_block['edit_info']['edited_by'])
setattr(xblock, '_published_on', published_block['edit_info']['edited_on'])
...@@ -13,6 +13,8 @@ from uuid import uuid4 ...@@ -13,6 +13,8 @@ from uuid import uuid4
# before importing the module # before importing the module
# TODO remove this import and the configuration -- xmodule should not depend on django! # TODO remove this import and the configuration -- xmodule should not depend on django!
from django.conf import settings from django.conf import settings
from xmodule.modulestore.edit_info import EditInfoMixin
if not settings.configured: if not settings.configured:
settings.configure() settings.configure()
...@@ -52,6 +54,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -52,6 +54,7 @@ class TestMixedModuleStore(unittest.TestCase):
'default_class': DEFAULT_CLASS, 'default_class': DEFAULT_CLASS,
'fs_root': DATA_DIR, 'fs_root': DATA_DIR,
'render_template': RENDER_TEMPLATE, 'render_template': RENDER_TEMPLATE,
'xblock_mixins': (EditInfoMixin,)
} }
DOC_STORE_CONFIG = { DOC_STORE_CONFIG = {
'host': HOST, 'host': HOST,
...@@ -995,7 +998,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -995,7 +998,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(
...@@ -1121,6 +1124,147 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -1121,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_date 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_date)
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
...@@ -829,7 +732,7 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -829,7 +732,7 @@ class TestMongoModuleStore(unittest.TestCase):
# Retrieve the block and verify its fields # Retrieve the block and verify its fields
component = self.draft_store.get_item(location) component = self.draft_store.get_item(location)
self.assertEqual(component.published_date, published_date) self.assertEqual(component.published_on, published_date)
self.assertEqual(component.published_by, published_by) self.assertEqual(component.published_by, published_by)
def test_export_course_with_peer_component(self): def test_export_course_with_peer_component(self):
......
...@@ -23,6 +23,7 @@ from xmodule.fields import Date, Timedelta ...@@ -23,6 +23,7 @@ from xmodule.fields import Date, Timedelta
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
from xmodule.modulestore.tests.test_modulestore import check_has_course_method from xmodule.modulestore.tests.test_modulestore import check_has_course_method
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
BRANCH_NAME_DRAFT = ModuleStoreEnum.BranchName.draft BRANCH_NAME_DRAFT = ModuleStoreEnum.BranchName.draft
...@@ -45,7 +46,7 @@ class SplitModuleTest(unittest.TestCase): ...@@ -45,7 +46,7 @@ class SplitModuleTest(unittest.TestCase):
modulestore_options = { modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': '', 'fs_root': '',
'xblock_mixins': (InheritanceMixin, XModuleMixin) 'xblock_mixins': (InheritanceMixin, XModuleMixin, EditInfoMixin)
} }
MODULESTORE = { MODULESTORE = {
......
...@@ -724,7 +724,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -724,7 +724,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
# leaving off original_version since it complicates creation w/o any obv value yet and is computable # leaving off original_version since it complicates creation w/o any obv value yet and is computable
# by following previous until None # by following previous until None
# definition_locator is only used by mongostores which separate definitions from blocks # definition_locator is only used by mongostores which separate definitions from blocks
self.edited_by = self.edited_on = self.previous_version = self.update_version = self.definition_locator = None self.previous_version = self.update_version = self.definition_locator = None
self.xmodule_runtime = None self.xmodule_runtime = None
@classmethod @classmethod
......
...@@ -124,7 +124,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -124,7 +124,7 @@ class XmlDescriptor(XModuleDescriptor):
# import and export. # import and export.
metadata_to_strip = ('data_dir', metadata_to_strip = ('data_dir',
'tabs', 'grading_policy', 'published_by', 'published_date', 'tabs', 'grading_policy',
'discussion_blackouts', 'discussion_blackouts',
# VS[compat] -- remove the below attrs once everything is in the CMS # VS[compat] -- remove the below attrs once everything is in the CMS
'course', 'org', 'url_name', 'filename', 'course', 'org', 'url_name', 'filename',
......
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