Merge pull request #1045 from edx/assets/persist_lock
save lock state in contentstore
This commit is contained in:
@@ -10,7 +10,6 @@ from django.views.decorators.http import require_POST
|
|||||||
|
|
||||||
from mitxmako.shortcuts import render_to_response
|
from mitxmako.shortcuts import render_to_response
|
||||||
from cache_toolbox.core import del_cached_content
|
from cache_toolbox.core import del_cached_content
|
||||||
from auth.authz import create_all_course_groups
|
|
||||||
|
|
||||||
from xmodule.contentstore.django import contentstore
|
from xmodule.contentstore.django import contentstore
|
||||||
from xmodule.modulestore.django import modulestore
|
from xmodule.modulestore.django import modulestore
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ from PIL import Image
|
|||||||
|
|
||||||
class StaticContent(object):
|
class StaticContent(object):
|
||||||
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None,
|
def __init__(self, loc, name, content_type, data, last_modified_at=None, thumbnail_location=None, import_path=None,
|
||||||
length=None):
|
length=None, locked=False):
|
||||||
self.location = loc
|
self.location = loc
|
||||||
self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed
|
self.name = name # a display string which can be edited, and thus not part of the location which needs to be fixed
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
self._data = data
|
self._data = data
|
||||||
self.length = length
|
self.length = length
|
||||||
@@ -26,6 +26,7 @@ class StaticContent(object):
|
|||||||
# optional information about where this file was imported from. This is needed to support import/export
|
# optional information about where this file was imported from. This is needed to support import/export
|
||||||
# cycles
|
# cycles
|
||||||
self.import_path = import_path
|
self.import_path = import_path
|
||||||
|
self.locked = locked
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_thumbnail(self):
|
def is_thumbnail(self):
|
||||||
@@ -133,10 +134,10 @@ class StaticContent(object):
|
|||||||
|
|
||||||
class StaticContentStream(StaticContent):
|
class StaticContentStream(StaticContent):
|
||||||
def __init__(self, loc, name, content_type, stream, last_modified_at=None, thumbnail_location=None, import_path=None,
|
def __init__(self, loc, name, content_type, stream, last_modified_at=None, thumbnail_location=None, import_path=None,
|
||||||
length=None):
|
length=None, locked=False):
|
||||||
super(StaticContentStream, self).__init__(loc, name, content_type, None, last_modified_at=last_modified_at,
|
super(StaticContentStream, self).__init__(loc, name, content_type, None, last_modified_at=last_modified_at,
|
||||||
thumbnail_location=thumbnail_location, import_path=import_path,
|
thumbnail_location=thumbnail_location, import_path=import_path,
|
||||||
length=length)
|
length=length, locked=locked)
|
||||||
self._stream = stream
|
self._stream = stream
|
||||||
|
|
||||||
def stream_data(self):
|
def stream_data(self):
|
||||||
@@ -153,7 +154,7 @@ class StaticContentStream(StaticContent):
|
|||||||
self._stream.seek(0)
|
self._stream.seek(0)
|
||||||
content = StaticContent(self.location, self.name, self.content_type, self._stream.read(),
|
content = StaticContent(self.location, self.name, self.content_type, self._stream.read(),
|
||||||
last_modified_at=self.last_modified_at, thumbnail_location=self.thumbnail_location,
|
last_modified_at=self.last_modified_at, thumbnail_location=self.thumbnail_location,
|
||||||
import_path=self.import_path, length=self.length)
|
import_path=self.import_path, length=self.length, locked=self.locked)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,17 +24,19 @@ class MongoContentStore(ContentStore):
|
|||||||
|
|
||||||
self.fs = gridfs.GridFS(_db, bucket)
|
self.fs = gridfs.GridFS(_db, bucket)
|
||||||
|
|
||||||
self.fs_files = _db[bucket + ".files"] # the underlying collection GridFS uses
|
self.fs_files = _db[bucket + ".files"] # the underlying collection GridFS uses
|
||||||
|
|
||||||
def save(self, content):
|
def save(self, content):
|
||||||
id = content.get_id()
|
content_id = content.get_id()
|
||||||
|
|
||||||
# Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair
|
# Seems like with the GridFS we can't update existing ID's we have to do a delete/add pair
|
||||||
self.delete(id)
|
self.delete(content_id)
|
||||||
|
|
||||||
with self.fs.new_file(_id=id, filename=content.get_url_path(), content_type=content.content_type,
|
with self.fs.new_file(_id=content_id, filename=content.get_url_path(), content_type=content.content_type,
|
||||||
displayname=content.name, thumbnail_location=content.thumbnail_location,
|
displayname=content.name, thumbnail_location=content.thumbnail_location,
|
||||||
import_path=content.import_path) as fp:
|
import_path=content.import_path,
|
||||||
|
# getattr b/c caching may mean some pickled instances don't have attr
|
||||||
|
locked=getattr(content, 'locked', False)) as fp:
|
||||||
if hasattr(content.data, '__iter__'):
|
if hasattr(content.data, '__iter__'):
|
||||||
for chunk in content.data:
|
for chunk in content.data:
|
||||||
fp.write(chunk)
|
fp.write(chunk)
|
||||||
@@ -43,25 +45,29 @@ class MongoContentStore(ContentStore):
|
|||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def delete(self, id):
|
def delete(self, content_id):
|
||||||
if self.fs.exists({"_id": id}):
|
if self.fs.exists({"_id": content_id}):
|
||||||
self.fs.delete(id)
|
self.fs.delete(content_id)
|
||||||
|
|
||||||
def find(self, location, throw_on_not_found=True, as_stream=False):
|
def find(self, location, throw_on_not_found=True, as_stream=False):
|
||||||
id = StaticContent.get_id_from_location(location)
|
content_id = StaticContent.get_id_from_location(location)
|
||||||
try:
|
try:
|
||||||
if as_stream:
|
if as_stream:
|
||||||
fp = self.fs.get(id)
|
fp = self.fs.get(content_id)
|
||||||
return StaticContentStream(location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
|
return StaticContentStream(
|
||||||
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
|
location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
|
||||||
import_path=fp.import_path if hasattr(fp, 'import_path') else None,
|
thumbnail_location=getattr(fp, 'thumbnail_location', None),
|
||||||
length=fp.length)
|
import_path=getattr(fp, 'import_path', None),
|
||||||
|
length=fp.length, locked=getattr(fp, 'locked', False)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
with self.fs.get(id) as fp:
|
with self.fs.get(content_id) as fp:
|
||||||
return StaticContent(location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate,
|
return StaticContent(
|
||||||
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
|
location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
|
||||||
import_path=fp.import_path if hasattr(fp, 'import_path') else None,
|
thumbnail_location=getattr(fp, 'thumbnail_location', None),
|
||||||
length=fp.length)
|
import_path=getattr(fp, 'import_path', None),
|
||||||
|
length=fp.length, locked=getattr(fp, 'locked', False)
|
||||||
|
)
|
||||||
except NoFile:
|
except NoFile:
|
||||||
if throw_on_not_found:
|
if throw_on_not_found:
|
||||||
raise NotFoundError()
|
raise NotFoundError()
|
||||||
@@ -69,9 +75,9 @@ class MongoContentStore(ContentStore):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_stream(self, location):
|
def get_stream(self, location):
|
||||||
id = StaticContent.get_id_from_location(location)
|
content_id = StaticContent.get_id_from_location(location)
|
||||||
try:
|
try:
|
||||||
handle = self.fs.get(id)
|
handle = self.fs.get(content_id)
|
||||||
except NoFile:
|
except NoFile:
|
||||||
raise NotFoundError()
|
raise NotFoundError()
|
||||||
|
|
||||||
@@ -135,3 +141,61 @@ class MongoContentStore(ContentStore):
|
|||||||
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
|
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
|
||||||
items = self.fs_files.find(location_to_query(course_filter))
|
items = self.fs_files.find(location_to_query(course_filter))
|
||||||
return list(items)
|
return list(items)
|
||||||
|
|
||||||
|
def set_attr(self, location, attr, value=True):
|
||||||
|
"""
|
||||||
|
Add/set the given attr on the asset at the given location. Does not allow overwriting gridFS built in
|
||||||
|
attrs such as _id, md5, uploadDate, length. Value can be any type which pymongo accepts.
|
||||||
|
|
||||||
|
Returns nothing
|
||||||
|
|
||||||
|
Raises NotFoundError if no such item exists
|
||||||
|
Raises AttributeError is attr is one of the build in attrs.
|
||||||
|
|
||||||
|
:param location: a c4x asset location
|
||||||
|
:param attr: which attribute to set
|
||||||
|
:param value: the value to set it to (any type pymongo accepts such as datetime, number, string)
|
||||||
|
"""
|
||||||
|
self.set_attrs(location, {attr: value})
|
||||||
|
|
||||||
|
def get_attr(self, location, attr, default=None):
|
||||||
|
"""
|
||||||
|
Get the value of attr set on location. If attr is unset, it returns default. Unlike set, this accessor
|
||||||
|
does allow getting the value of reserved keywords.
|
||||||
|
:param location: a c4x asset location
|
||||||
|
"""
|
||||||
|
return self.get_attrs(location).get(attr, default)
|
||||||
|
|
||||||
|
def set_attrs(self, location, attr_dict):
|
||||||
|
"""
|
||||||
|
Like set_attr but sets multiple key value pairs.
|
||||||
|
|
||||||
|
Returns nothing.
|
||||||
|
|
||||||
|
Raises NotFoundError if no such item exists
|
||||||
|
Raises AttributeError is attr_dict has any attrs which are one of the build in attrs.
|
||||||
|
|
||||||
|
:param location: a c4x asset location
|
||||||
|
"""
|
||||||
|
for attr in attr_dict.iterkeys():
|
||||||
|
if attr in ['_id', 'md5', 'uploadDate', 'length']:
|
||||||
|
raise AttributeError("{} is a protected attribute.".format(attr))
|
||||||
|
item = self.fs_files.find_one(location_to_query(location))
|
||||||
|
if item is None:
|
||||||
|
raise NotFoundError()
|
||||||
|
self.fs_files.update({"_id": item["_id"]}, {"$set": attr_dict})
|
||||||
|
|
||||||
|
def get_attrs(self, location):
|
||||||
|
"""
|
||||||
|
Gets all of the attributes associated with the given asset. Note, returns even built in attrs
|
||||||
|
such as md5 which you cannot resubmit in an update; so, don't call set_attrs with the result of this
|
||||||
|
but only with the set of attrs you want to explicitly update.
|
||||||
|
|
||||||
|
The attrs will be a superset of _id, contentType, chunkSize, filename, uploadDate, & md5
|
||||||
|
|
||||||
|
:param location: a c4x asset location
|
||||||
|
"""
|
||||||
|
item = self.fs_files.find_one(location_to_query(location))
|
||||||
|
if item is None:
|
||||||
|
raise NotFoundError()
|
||||||
|
return item
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
|
|||||||
from xmodule.contentstore.mongo import MongoContentStore
|
from xmodule.contentstore.mongo import MongoContentStore
|
||||||
|
|
||||||
from xmodule.modulestore.tests.test_modulestore import check_path_to_location
|
from xmodule.modulestore.tests.test_modulestore import check_path_to_location
|
||||||
|
from IPython.testing.nose_assert_methods import assert_in, assert_not_in
|
||||||
|
from xmodule.exceptions import NotFoundError
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -203,6 +205,55 @@ class TestMongoModuleStore(object):
|
|||||||
assert_equals('Resources', get_tab_name(3))
|
assert_equals('Resources', get_tab_name(3))
|
||||||
assert_equals('Discussion', get_tab_name(4))
|
assert_equals('Discussion', get_tab_name(4))
|
||||||
|
|
||||||
|
def test_contentstore_attrs(self):
|
||||||
|
"""
|
||||||
|
Test getting, setting, and defaulting the locked attr and arbitrary attrs.
|
||||||
|
"""
|
||||||
|
location = Location('i4x', 'edX', 'toy', 'course', '2012_Fall')
|
||||||
|
course_content = TestMongoModuleStore.content_store.get_all_content_for_course(location)
|
||||||
|
assert len(course_content) > 0
|
||||||
|
# a bit overkill, could just do for content[0]
|
||||||
|
for content in course_content:
|
||||||
|
assert not content.get('locked', False)
|
||||||
|
assert not TestMongoModuleStore.content_store.get_attr(content['_id'], 'locked', False)
|
||||||
|
attrs = TestMongoModuleStore.content_store.get_attrs(content['_id'])
|
||||||
|
assert_in('uploadDate', attrs)
|
||||||
|
assert not attrs.get('locked', False)
|
||||||
|
TestMongoModuleStore.content_store.set_attr(content['_id'], 'locked', True)
|
||||||
|
assert TestMongoModuleStore.content_store.get_attr(content['_id'], 'locked', False)
|
||||||
|
attrs = TestMongoModuleStore.content_store.get_attrs(content['_id'])
|
||||||
|
assert_in('locked', attrs)
|
||||||
|
assert attrs['locked'] is True
|
||||||
|
TestMongoModuleStore.content_store.set_attrs(content['_id'], {'miscel': 99})
|
||||||
|
assert_equals(TestMongoModuleStore.content_store.get_attr(content['_id'], 'miscel'), 99)
|
||||||
|
assert_raises(
|
||||||
|
AttributeError, TestMongoModuleStore.content_store.set_attr, course_content[0],
|
||||||
|
'md5', 'ff1532598830e3feac91c2449eaa60d6'
|
||||||
|
)
|
||||||
|
assert_raises(
|
||||||
|
AttributeError, TestMongoModuleStore.content_store.set_attrs, course_content[0],
|
||||||
|
{'foo': 9, 'md5': 'ff1532598830e3feac91c2449eaa60d6'}
|
||||||
|
)
|
||||||
|
assert_raises(
|
||||||
|
NotFoundError, TestMongoModuleStore.content_store.get_attr,
|
||||||
|
Location('bogus', 'bogus', 'bogus', 'asset', 'bogus'),
|
||||||
|
'displayname'
|
||||||
|
)
|
||||||
|
assert_raises(
|
||||||
|
NotFoundError, TestMongoModuleStore.content_store.set_attr,
|
||||||
|
Location('bogus', 'bogus', 'bogus', 'asset', 'bogus'),
|
||||||
|
'displayname', 'hello'
|
||||||
|
)
|
||||||
|
assert_raises(
|
||||||
|
NotFoundError, TestMongoModuleStore.content_store.get_attrs,
|
||||||
|
Location('bogus', 'bogus', 'bogus', 'asset', 'bogus')
|
||||||
|
)
|
||||||
|
assert_raises(
|
||||||
|
NotFoundError, TestMongoModuleStore.content_store.set_attrs,
|
||||||
|
Location('bogus', 'bogus', 'bogus', 'asset', 'bogus'),
|
||||||
|
{'displayname': 'hello'}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestMongoKeyValueStore(object):
|
class TestMongoKeyValueStore(object):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user