Commit 464e0c77 by Don Mitchell

Merge pull request #1045 from edx/assets/persist_lock

save lock state in contentstore
parents d8af16dc 7d13eb8f
...@@ -10,7 +10,6 @@ from django.views.decorators.http import require_POST ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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): ...@@ -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):
""" """
......
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