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
from mitxmako.shortcuts import render_to_response
from cache_toolbox.core import del_cached_content
from auth.authz import create_all_course_groups
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore
......
......@@ -15,9 +15,9 @@ from PIL import Image
class StaticContent(object):
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.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._data = data
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
# cycles
self.import_path = import_path
self.locked = locked
@property
def is_thumbnail(self):
......@@ -133,10 +134,10 @@ class StaticContent(object):
class StaticContentStream(StaticContent):
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,
thumbnail_location=thumbnail_location, import_path=import_path,
length=length)
length=length, locked=locked)
self._stream = stream
def stream_data(self):
......@@ -153,7 +154,7 @@ class StaticContentStream(StaticContent):
self._stream.seek(0)
content = StaticContent(self.location, self.name, self.content_type, self._stream.read(),
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
......
......@@ -24,17 +24,19 @@ class MongoContentStore(ContentStore):
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):
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
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,
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__'):
for chunk in content.data:
fp.write(chunk)
......@@ -43,25 +45,29 @@ class MongoContentStore(ContentStore):
return content
def delete(self, id):
if self.fs.exists({"_id": id}):
self.fs.delete(id)
def delete(self, content_id):
if self.fs.exists({"_id": content_id}):
self.fs.delete(content_id)
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:
if as_stream:
fp = self.fs.get(id)
return StaticContentStream(location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path=fp.import_path if hasattr(fp, 'import_path') else None,
length=fp.length)
fp = self.fs.get(content_id)
return StaticContentStream(
location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
thumbnail_location=getattr(fp, 'thumbnail_location', None),
import_path=getattr(fp, 'import_path', None),
length=fp.length, locked=getattr(fp, 'locked', False)
)
else:
with self.fs.get(id) as fp:
return StaticContent(location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate,
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
import_path=fp.import_path if hasattr(fp, 'import_path') else None,
length=fp.length)
with self.fs.get(content_id) as fp:
return StaticContent(
location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
thumbnail_location=getattr(fp, 'thumbnail_location', None),
import_path=getattr(fp, 'import_path', None),
length=fp.length, locked=getattr(fp, 'locked', False)
)
except NoFile:
if throw_on_not_found:
raise NotFoundError()
......@@ -69,9 +75,9 @@ class MongoContentStore(ContentStore):
return None
def get_stream(self, location):
id = StaticContent.get_id_from_location(location)
content_id = StaticContent.get_id_from_location(location)
try:
handle = self.fs.get(id)
handle = self.fs.get(content_id)
except NoFile:
raise NotFoundError()
......@@ -135,3 +141,61 @@ class MongoContentStore(ContentStore):
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
items = self.fs_files.find(location_to_query(course_filter))
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.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__)
......@@ -203,6 +205,55 @@ class TestMongoModuleStore(object):
assert_equals('Resources', get_tab_name(3))
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):
"""
......
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