Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
5ffc6ac2
Commit
5ffc6ac2
authored
Oct 24, 2014
by
John Eskew
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #4854 from edx/jeskew/assetstore_modulestore_work
Phase 1 of adding asset metadata saving to old Mongo
parents
3d1c54fe
b857a0ed
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
1215 additions
and
16 deletions
+1215
-16
common/lib/xmodule/xmodule/assetstore/__init__.py
+168
-0
common/lib/xmodule/xmodule/modulestore/__init__.py
+276
-3
common/lib/xmodule/xmodule/modulestore/mixed.py
+210
-1
common/lib/xmodule/xmodule/modulestore/mongo/base.py
+155
-3
common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py
+5
-1
common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
+2
-0
common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
+398
-8
lms/envs/common.py
+1
-0
No files found.
common/lib/xmodule/xmodule/assetstore/__init__.py
0 → 100644
View file @
5ffc6ac2
"""
Classes representing asset & asset thumbnail metadata.
"""
from
datetime
import
datetime
from
contracts
import
contract
,
new_contract
from
opaque_keys.edx.keys
import
CourseKey
,
AssetKey
new_contract
(
'AssetKey'
,
AssetKey
)
new_contract
(
'datetime'
,
datetime
)
class
IncorrectAssetIdType
(
Exception
):
"""
Raised when the asset ID passed-in to create an AssetMetadata or
AssetThumbnailMetadata is of the wrong type.
"""
pass
class
AssetMetadata
(
object
):
"""
Stores the metadata associated with a particular course asset. The asset metadata gets stored
in the modulestore.
"""
TOP_LEVEL_ATTRS
=
[
'basename'
,
'internal_name'
,
'locked'
,
'contenttype'
,
'md5'
]
EDIT_INFO_ATTRS
=
[
'curr_version'
,
'prev_version'
,
'edited_by'
,
'edited_on'
]
ALLOWED_ATTRS
=
TOP_LEVEL_ATTRS
+
EDIT_INFO_ATTRS
# All AssetMetadata objects should have AssetLocators with this type.
ASSET_TYPE
=
'asset'
@contract
(
asset_id
=
'AssetKey'
,
basename
=
'str | unicode | None'
,
internal_name
=
'str | None'
,
locked
=
'bool | None'
,
contenttype
=
'str | unicode | None'
,
md5
=
'str | None'
,
curr_version
=
'str | None'
,
prev_version
=
'str | None'
)
def
__init__
(
self
,
asset_id
,
basename
=
None
,
internal_name
=
None
,
locked
=
None
,
contenttype
=
None
,
md5
=
None
,
curr_version
=
None
,
prev_version
=
None
):
"""
Construct a AssetMetadata object.
Arguments:
asset_id (AssetKey): Key identifying this particular asset.
basename (str): Original path to file at asset upload time.
internal_name (str): Name under which the file is stored internally.
locked (bool): If True, only course participants can access the asset.
contenttype (str): MIME type of the asset.
curr_version (str): Current version of the asset.
prev_version (str): Previous version of the asset.
"""
if
asset_id
.
asset_type
!=
self
.
ASSET_TYPE
:
raise
IncorrectAssetIdType
()
self
.
asset_id
=
asset_id
self
.
basename
=
basename
# Path w/o filename.
self
.
internal_name
=
internal_name
self
.
locked
=
locked
self
.
contenttype
=
contenttype
self
.
md5
=
md5
self
.
curr_version
=
curr_version
self
.
prev_version
=
prev_version
self
.
edited_by
=
None
self
.
edited_on
=
None
def
__repr__
(
self
):
return
"""AssetMetadata{!r}"""
.
format
((
self
.
asset_id
,
self
.
basename
,
self
.
internal_name
,
self
.
locked
,
self
.
contenttype
,
self
.
md5
,
self
.
curr_version
,
self
.
prev_version
,
self
.
edited_by
,
self
.
edited_on
))
def
update
(
self
,
attr_dict
):
"""
Set the attributes on the metadata. Ignore all those outside the known fields.
Arguments:
attr_dict: Prop, val dictionary of all attributes to set.
"""
for
attr
,
val
in
attr_dict
.
iteritems
():
if
attr
in
self
.
ALLOWED_ATTRS
:
setattr
(
self
,
attr
,
val
)
def
to_mongo
(
self
):
"""
Converts metadata properties into a MongoDB-storable dict.
"""
return
{
'filename'
:
self
.
asset_id
.
path
,
'basename'
:
self
.
basename
,
'internal_name'
:
self
.
internal_name
,
'locked'
:
self
.
locked
,
'contenttype'
:
self
.
contenttype
,
'md5'
:
self
.
md5
,
'edit_info'
:
{
'curr_version'
:
self
.
curr_version
,
'prev_version'
:
self
.
prev_version
,
'edited_by'
:
self
.
edited_by
,
'edited_on'
:
self
.
edited_on
}
}
@contract
(
asset_doc
=
'dict | None'
)
def
from_mongo
(
self
,
asset_doc
):
"""
Fill in all metadata fields from a MongoDB document.
The asset_id prop is initialized upon construction only.
"""
if
asset_doc
is
None
:
return
self
.
basename
=
asset_doc
[
'basename'
]
self
.
internal_name
=
asset_doc
[
'internal_name'
]
self
.
locked
=
asset_doc
[
'locked'
]
self
.
contenttype
=
asset_doc
[
'contenttype'
]
self
.
md5
=
asset_doc
[
'md5'
]
edit_info
=
asset_doc
[
'edit_info'
]
self
.
curr_version
=
edit_info
[
'curr_version'
]
self
.
prev_version
=
edit_info
[
'prev_version'
]
self
.
edited_by
=
edit_info
[
'edited_by'
]
self
.
edited_on
=
edit_info
[
'edited_on'
]
class
AssetThumbnailMetadata
(
object
):
"""
Stores the metadata associated with the thumbnail of a course asset.
"""
# All AssetThumbnailMetadata objects should have AssetLocators with this type.
ASSET_TYPE
=
'thumbnail'
@contract
(
asset_id
=
'AssetKey'
,
internal_name
=
'str | unicode | None'
)
def
__init__
(
self
,
asset_id
,
internal_name
=
None
):
"""
Construct a AssetThumbnailMetadata object.
Arguments:
asset_id (AssetKey): Key identifying this particular asset.
internal_name (str): Name under which the file is stored internally.
"""
if
asset_id
.
asset_type
!=
self
.
ASSET_TYPE
:
raise
IncorrectAssetIdType
()
self
.
asset_id
=
asset_id
self
.
internal_name
=
internal_name
def
__repr__
(
self
):
return
"""AssetMetadata{!r}"""
.
format
((
self
.
asset_id
,
self
.
internal_name
))
def
to_mongo
(
self
):
"""
Converts metadata properties into a MongoDB-storable dict.
"""
return
{
'filename'
:
self
.
asset_id
.
path
,
'internal_name'
:
self
.
internal_name
}
@contract
(
thumbnail_doc
=
'dict | None'
)
def
from_mongo
(
self
,
thumbnail_doc
):
"""
Fill in all metadata fields from a MongoDB document.
The asset_id prop is initialized upon construction only.
"""
if
thumbnail_doc
is
None
:
return
self
.
internal_name
=
thumbnail_doc
[
'internal_name'
]
common/lib/xmodule/xmodule/modulestore/__init__.py
View file @
5ffc6ac2
...
...
@@ -12,23 +12,30 @@ from uuid import uuid4
from
collections
import
namedtuple
,
defaultdict
import
collections
from
contextlib
import
contextmanager
import
functools
import
threading
from
abc
import
ABCMeta
,
abstractmethod
from
contracts
import
contract
,
new_contract
from
xblock.plugin
import
default_select
from
.exceptions
import
InvalidLocationError
,
InsufficientSpecificationError
from
xmodule.errortracker
import
make_error_tracker
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
xmodule.assetstore
import
AssetMetadata
,
AssetThumbnailMetadata
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
,
AssetKey
from
opaque_keys.edx.locations
import
Location
# For import backwards compatibility
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
xblock.runtime
import
Mixologist
from
xblock.core
import
XBlock
import
functools
import
threading
log
=
logging
.
getLogger
(
'edx.modulestore'
)
new_contract
(
'CourseKey'
,
CourseKey
)
new_contract
(
'AssetKey'
,
AssetKey
)
new_contract
(
'AssetMetadata'
,
AssetMetadata
)
new_contract
(
'AssetThumbnailMetadata'
,
AssetThumbnailMetadata
)
class
ModuleStoreEnum
(
object
):
"""
...
...
@@ -740,6 +747,9 @@ class ModuleStoreReadBase(BulkOperationsMixin, ModuleStoreRead):
"""
@functools.wraps
(
func
)
def
wrapper
(
self
,
*
args
,
**
kwargs
):
"""
Wraps a method to memoize results.
"""
if
self
.
request_cache
:
cache_key
=
'&'
.
join
([
hashvalue
(
arg
)
for
arg
in
args
])
if
cache_key
in
self
.
request_cache
.
data
.
setdefault
(
func
.
__name__
,
{}):
...
...
@@ -863,6 +873,269 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
parent
.
children
.
append
(
item
.
location
)
self
.
update_item
(
parent
,
user_id
)
def
_find_course_assets
(
self
,
course_key
):
"""
Base method to override.
"""
raise
NotImplementedError
()
def
_find_course_asset
(
self
,
course_key
,
filename
,
get_thumbnail
=
False
):
"""
Internal; finds or creates course asset info -and- finds existing asset (or thumbnail) metadata.
Arguments:
course_key (CourseKey): course identifier
filename (str): filename of the asset or thumbnail
get_thumbnail (bool): True gets thumbnail data, False gets asset data
Returns:
Asset info for the course, index of asset/thumbnail in list (None if asset/thumbnail does not exist)
"""
course_assets
=
self
.
_find_course_assets
(
course_key
)
if
get_thumbnail
:
all_assets
=
course_assets
[
'thumbnails'
]
else
:
all_assets
=
course_assets
[
'assets'
]
# See if this asset already exists by checking the external_filename.
# Studio doesn't currently support using multiple course assets with the same filename.
# So use the filename as the unique identifier.
for
idx
,
asset
in
enumerate
(
all_assets
):
if
asset
[
'filename'
]
==
filename
:
return
course_assets
,
idx
return
course_assets
,
None
def
_save_asset_info
(
self
,
course_key
,
asset_metadata
,
user_id
,
thumbnail
=
False
):
"""
Base method to over-ride in modulestore.
"""
raise
NotImplementedError
()
@contract
(
course_key
=
'CourseKey'
,
asset_metadata
=
'AssetMetadata'
,
user_id
=
'str | unicode'
)
def
save_asset_metadata
(
self
,
course_key
,
asset_metadata
,
user_id
):
"""
Saves the asset metadata for a particular course's asset.
Arguments:
course_key (CourseKey): course identifier
asset_metadata (AssetMetadata): data about the course asset data
Returns:
True if metadata save was successful, else False
"""
return
self
.
_save_asset_info
(
course_key
,
asset_metadata
,
user_id
,
thumbnail
=
False
)
@contract
(
course_key
=
'CourseKey'
,
asset_thumbnail_metadata
=
'AssetThumbnailMetadata'
)
def
save_asset_thumbnail_metadata
(
self
,
course_key
,
asset_thumbnail_metadata
):
"""
Saves the asset thumbnail metadata for a particular course asset's thumbnail.
Arguments:
course_key (CourseKey): course identifier
asset_thumbnail_metadata (AssetThumbnailMetadata): data about the course asset thumbnail
Returns:
True if thumbnail metadata save was successful, else False
"""
return
self
.
_save_asset_info
(
course_key
,
asset_thumbnail_metadata
,
''
,
thumbnail
=
True
)
@contract
(
asset_key
=
'AssetKey'
)
def
_find_asset_info
(
self
,
asset_key
,
thumbnail
=
False
):
"""
Find the info for a particular course asset/thumbnail.
Arguments:
asset_key (AssetKey): key containing original asset filename
thumbnail (bool): True if finding thumbnail, False if finding asset metadata
Returns:
asset/thumbnail metadata (AssetMetadata/AssetThumbnailMetadata) -or- None if not found
"""
course_assets
,
asset_idx
=
self
.
_find_course_asset
(
asset_key
.
course_key
,
asset_key
.
path
,
thumbnail
)
if
asset_idx
is
None
:
return
None
if
thumbnail
:
info
=
'thumbnails'
mdata
=
AssetThumbnailMetadata
(
asset_key
,
asset_key
.
path
)
else
:
info
=
'assets'
mdata
=
AssetMetadata
(
asset_key
,
asset_key
.
path
)
all_assets
=
course_assets
[
info
]
mdata
.
from_mongo
(
all_assets
[
asset_idx
])
return
mdata
@contract
(
asset_key
=
'AssetKey'
)
def
find_asset_metadata
(
self
,
asset_key
):
"""
Find the metadata for a particular course asset.
Arguments:
asset_key (AssetKey): key containing original asset filename
Returns:
asset metadata (AssetMetadata) -or- None if not found
"""
return
self
.
_find_asset_info
(
asset_key
,
thumbnail
=
False
)
@contract
(
asset_key
=
'AssetKey'
)
def
find_asset_thumbnail_metadata
(
self
,
asset_key
):
"""
Find the metadata for a particular course asset.
Arguments:
asset_key (AssetKey): key containing original asset filename
Returns:
asset metadata (AssetMetadata) -or- None if not found
"""
return
self
.
_find_asset_info
(
asset_key
,
thumbnail
=
True
)
@contract
(
course_key
=
'CourseKey'
,
start
=
'int | None'
,
maxresults
=
'int | None'
,
sort
=
'list | None'
,
get_thumbnails
=
'bool'
)
def
_get_all_asset_metadata
(
self
,
course_key
,
start
=
0
,
maxresults
=-
1
,
sort
=
None
,
get_thumbnails
=
False
):
"""
Returns a list of static asset (or thumbnail) metadata for a course.
Args:
course_key (CourseKey): course identifier
start (int): optional - start at this asset number
maxresults (int): optional - return at most this many, -1 means no limit
sort (array): optional - None means no sort
(sort_by (str), sort_order (str))
sort_by - one of 'uploadDate' or 'displayname'
sort_order - one of 'ascending' or 'descending'
get_thumbnails (bool): True if getting thumbnail metadata, else getting asset metadata
Returns:
List of AssetMetadata or AssetThumbnailMetadata objects.
"""
course_assets
=
self
.
_find_course_assets
(
course_key
)
if
course_assets
is
None
:
# If no course assets are found, return None instead of empty list
# to distinguish zero assets from "not able to retrieve assets".
return
None
if
get_thumbnails
:
all_assets
=
course_assets
[
'thumbnails'
]
else
:
all_assets
=
course_assets
[
'assets'
]
# DO_NEXT: Add start/maxresults/sort functionality as part of https://openedx.atlassian.net/browse/PLAT-74
if
start
and
maxresults
and
sort
:
pass
ret_assets
=
[]
for
asset
in
all_assets
:
if
get_thumbnails
:
thumb
=
AssetThumbnailMetadata
(
course_key
.
make_asset_key
(
'thumbnail'
,
asset
[
'filename'
]),
internal_name
=
asset
[
'filename'
])
ret_assets
.
append
(
thumb
)
else
:
one_asset
=
AssetMetadata
(
course_key
.
make_asset_key
(
'asset'
,
asset
[
'filename'
]))
one_asset
.
from_mongo
(
asset
)
ret_assets
.
append
(
one_asset
)
return
ret_assets
@contract
(
course_key
=
'CourseKey'
,
start
=
'int | None'
,
maxresults
=
'int | None'
,
sort
=
'list | None'
)
def
get_all_asset_metadata
(
self
,
course_key
,
start
=
0
,
maxresults
=-
1
,
sort
=
None
):
"""
Returns a list of static assets for a course.
By default all assets are returned, but start and maxresults can be provided to limit the query.
Args:
course_key (CourseKey): course identifier
start (int): optional - start at this asset number
maxresults (int): optional - return at most this many, -1 means no limit
sort (array): optional - None means no sort
(sort_by (str), sort_order (str))
sort_by - one of 'uploadDate' or 'displayname'
sort_order - one of 'ascending' or 'descending'
Returns:
List of AssetMetadata objects.
"""
return
self
.
_get_all_asset_metadata
(
course_key
,
start
,
maxresults
,
sort
,
get_thumbnails
=
False
)
@contract
(
course_key
=
'CourseKey'
)
def
get_all_asset_thumbnail_metadata
(
self
,
course_key
):
"""
Returns a list of thumbnails for all course assets.
Args:
course_key (CourseKey): course identifier
Returns:
List of AssetThumbnailMetadata objects.
"""
return
self
.
_get_all_asset_metadata
(
course_key
,
get_thumbnails
=
True
)
def
set_asset_metadata_attrs
(
self
,
asset_key
,
attrs
,
user_id
):
"""
Base method to over-ride in modulestore.
"""
raise
NotImplementedError
()
def
_delete_asset_data
(
self
,
asset_key
,
thumbnail
=
False
):
"""
Base method to over-ride in modulestore.
"""
raise
NotImplementedError
()
@contract
(
asset_key
=
'AssetKey'
,
attr
=
str
)
def
set_asset_metadata_attr
(
self
,
asset_key
,
attr
,
value
,
user_id
):
"""
Add/set the given attr on the asset at the given location. Value can be any type which pymongo accepts.
Arguments:
asset_key (AssetKey): asset identifier
attr (str): which attribute to set
value: the value to set it to (any type pymongo accepts such as datetime, number, string)
Raises:
ItemNotFoundError if no such item exists
AttributeError is attr is one of the build in attrs.
"""
return
self
.
set_asset_metadata_attrs
(
asset_key
,
{
attr
:
value
},
user_id
)
@contract
(
asset_key
=
'AssetKey'
)
def
delete_asset_metadata
(
self
,
asset_key
):
"""
Deletes a single asset's metadata.
Arguments:
asset_key (AssetKey): locator containing original asset filename
Returns:
Number of asset metadata entries deleted (0 or 1)
"""
return
self
.
_delete_asset_data
(
asset_key
,
thumbnail
=
False
)
@contract
(
asset_key
=
'AssetKey'
)
def
delete_asset_thumbnail_metadata
(
self
,
asset_key
):
"""
Deletes a single asset's metadata.
Arguments:
asset_key (AssetKey): locator containing original asset filename
Returns:
Number of asset metadata entries deleted (0 or 1)
"""
return
self
.
_delete_asset_data
(
asset_key
,
thumbnail
=
True
)
@contract
(
source_course_key
=
'CourseKey'
,
dest_course_key
=
'CourseKey'
)
def
copy_all_asset_metadata
(
self
,
source_course_key
,
dest_course_key
):
"""
Copy all the course assets from source_course_key to dest_course_key.
Arguments:
source_course_key (CourseKey): identifier of course to copy from
dest_course_key (CourseKey): identifier of course to copy to
"""
pass
def
only_xmodules
(
identifier
,
entry_points
):
"""Only use entry_points that are supplied by the xmodule package"""
...
...
common/lib/xmodule/xmodule/modulestore/mixed.py
View file @
5ffc6ac2
...
...
@@ -9,10 +9,12 @@ import logging
from
contextlib
import
contextmanager
import
itertools
import
functools
from
contracts
import
contract
,
new_contract
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
,
AssetKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
xmodule.assetstore
import
AssetMetadata
,
AssetThumbnailMetadata
from
.
import
ModuleStoreWriteBase
from
.
import
ModuleStoreEnum
...
...
@@ -20,6 +22,10 @@ from .exceptions import ItemNotFoundError, DuplicateCourseError
from
.draft_and_published
import
ModuleStoreDraftAndPublished
from
.split_migrator
import
SplitMigrator
new_contract
(
'CourseKey'
,
CourseKey
)
new_contract
(
'AssetKey'
,
AssetKey
)
new_contract
(
'AssetMetadata'
,
AssetMetadata
)
new_contract
(
'AssetThumbnailMetadata'
,
AssetThumbnailMetadata
)
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -309,6 +315,209 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
store
=
self
.
_get_modulestore_for_courseid
(
course_key
)
return
store
.
delete_course
(
course_key
,
user_id
)
def
_save_asset_info
(
self
,
course_key
,
asset_metadata
,
user_id
,
thumbnail
=
False
):
"""
Base method to over-ride in modulestore.
"""
raise
NotImplementedError
()
def
_delete_asset_data
(
self
,
asset_key
,
thumbnail
=
False
):
"""
Base method to over-ride in modulestore.
"""
raise
NotImplementedError
()
def
_find_course_assets
(
self
,
course_key
):
"""
Base method to override.
"""
raise
NotImplementedError
()
@contract
(
course_key
=
'CourseKey'
,
asset_metadata
=
'AssetMetadata'
)
def
save_asset_metadata
(
self
,
course_key
,
asset_metadata
,
user_id
):
"""
Saves the asset metadata for a particular course's asset.
Args:
course_key (CourseKey): course identifier
asset_metadata (AssetMetadata): data about the course asset data
Returns:
bool: True if metadata save was successful, else False
"""
store
=
self
.
_get_modulestore_for_courseid
(
course_key
)
return
store
.
save_asset_metadata
(
course_key
,
asset_metadata
,
user_id
)
@contract
(
course_key
=
'CourseKey'
,
asset_thumbnail_metadata
=
'AssetThumbnailMetadata'
)
def
save_asset_thumbnail_metadata
(
self
,
course_key
,
asset_thumbnail_metadata
):
"""
Saves the asset thumbnail metadata for a particular course asset's thumbnail.
Arguments:
course_key (CourseKey): course identifier
asset_thumbnail_metadata (AssetThumbnailMetadata): data about the course asset thumbnail
Returns:
True if thumbnail metadata save was successful, else False
"""
store
=
self
.
_get_modulestore_for_courseid
(
course_key
)
return
store
.
save_asset_metadata
(
course_key
,
asset_thumbnail_metadata
)
@contract
(
asset_key
=
'AssetKey'
)
def
find_asset_metadata
(
self
,
asset_key
):
"""
Find the metadata for a particular course asset.
Args:
asset_key (AssetKey): locator containing original asset filename
Returns:
asset metadata (AssetMetadata) -or- None if not found
"""
store
=
self
.
_get_modulestore_for_courseid
(
asset_key
.
course_key
)
return
store
.
find_asset_metadata
(
asset_key
)
@contract
(
asset_key
=
'AssetKey'
)
def
find_asset_thumbnail_metadata
(
self
,
asset_key
):
"""
Find the metadata for a particular course asset.
Arguments:
asset_key (AssetKey): key containing original asset filename
Returns:
asset metadata (AssetMetadata) -or- None if not found
"""
store
=
self
.
_get_modulestore_for_courseid
(
asset_key
.
course_key
)
return
store
.
find_asset_thumbnail_metadata
(
asset_key
)
@contract
(
course_key
=
'CourseKey'
,
start
=
int
,
maxresults
=
int
,
sort
=
'list | None'
)
def
get_all_asset_metadata
(
self
,
course_key
,
start
=
0
,
maxresults
=-
1
,
sort
=
None
):
"""
Returns a list of static assets for a course.
By default all assets are returned, but start and maxresults can be provided to limit the query.
Args:
course_key (CourseKey): course identifier
start (int): optional - start at this asset number
maxresults (int): optional - return at most this many, -1 means no limit
sort (array): optional - None means no sort
(sort_by (str), sort_order (str))
sort_by - one of 'uploadDate' or 'displayname'
sort_order - one of 'ascending' or 'descending'
Returns:
List of asset data dictionaries, which have the following keys:
asset_key (AssetKey): asset identifier
displayname: The human-readable name of the asset
uploadDate (datetime.datetime): The date and time that the file was uploaded
contentType: The mimetype string of the asset
md5: An md5 hash of the asset content
"""
store
=
self
.
_get_modulestore_for_courseid
(
course_key
)
return
store
.
get_all_asset_metadata
(
course_key
,
start
,
maxresults
,
sort
)
@contract
(
course_key
=
'CourseKey'
)
def
get_all_asset_thumbnail_metadata
(
self
,
course_key
):
"""
Returns a list of thumbnails for all course assets.
Args:
course_key (CourseKey): course identifier
Returns:
List of AssetThumbnailMetadata objects.
"""
store
=
self
.
_get_modulestore_for_courseid
(
course_key
)
return
store
.
get_all_asset_thumbnail_metadata
(
course_key
)
@contract
(
asset_key
=
'AssetKey'
)
def
delete_asset_metadata
(
self
,
asset_key
):
"""
Deletes a single asset's metadata.
Arguments:
asset_id (AssetKey): locator containing original asset filename
Returns:
Number of asset metadata entries deleted (0 or 1)
"""
store
=
self
.
_get_modulestore_for_courseid
(
asset_key
.
course_key
)
return
store
.
delete_asset_metadata
(
asset_key
)
@contract
(
asset_key
=
'AssetKey'
)
def
delete_asset_thumbnail_metadata
(
self
,
asset_key
):
"""
Deletes a single asset's metadata.
Arguments:
asset_key (AssetKey): locator containing original asset filename
Returns:
Number of asset metadata entries deleted (0 or 1)
"""
store
=
self
.
_get_modulestore_for_courseid
(
asset_key
.
course_key
)
return
store
.
delete_asset_metadata
(
asset_key
)
@contract
(
course_key
=
'CourseKey'
)
def
delete_all_asset_metadata
(
self
,
course_key
):
"""
Delete all of the assets which use this course_key as an identifier.
Arguments:
course_key (CourseKey): course_identifier
"""
store
=
self
.
_get_modulestore_for_courseid
(
course_key
)
return
store
.
delete_all_asset_metadata
(
course_key
)
@contract
(
source_course_key
=
'CourseKey'
,
dest_course_key
=
'CourseKey'
)
def
copy_all_asset_metadata
(
self
,
source_course_key
,
dest_course_key
):
"""
Copy all the course assets from source_course_key to dest_course_key.
Arguments:
source_course_key (CourseKey): identifier of course to copy from
dest_course_key (CourseKey): identifier of course to copy to
"""
# When implementing this in https://openedx.atlassian.net/browse/PLAT-78 , consider this:
# Check the modulestores of both the source and dest course_keys. If in different modulestores,
# export all asset data from one modulestore and import it into the dest one.
store
=
self
.
_get_modulestore_for_courseid
(
source_course_key
)
return
store
.
copy_all_asset_metadata
(
source_course_key
,
dest_course_key
)
@contract
(
asset_key
=
'AssetKey'
,
attr
=
str
)
def
set_asset_metadata_attr
(
self
,
asset_key
,
attr
,
value
,
user_id
):
"""
Add/set the given attr on the asset at the given location. Value can be any type which pymongo accepts.
Arguments:
asset_key (AssetKey): asset identifier
attr (str): which attribute to set
value: the value to set it to (any type pymongo accepts such as datetime, number, string)
Raises:
NotFoundError if no such item exists
AttributeError is attr is one of the build in attrs.
"""
store
=
self
.
_get_modulestore_for_courseid
(
asset_key
.
course_key
)
return
store
.
set_asset_metadata_attrs
(
asset_key
,
attr
,
value
,
user_id
)
@contract
(
asset_key
=
'AssetKey'
,
attr_dict
=
dict
)
def
set_asset_metadata_attrs
(
self
,
asset_key
,
attr_dict
,
user_id
):
"""
Add/set the given dict of attrs on the asset at the given location. Value can be any type which pymongo accepts.
Arguments:
asset_key (AssetKey): asset identifier
attr_dict (dict): attribute/value pairs to set
Raises:
NotFoundError if no such item exists
AttributeError is attr is one of the build in attrs.
"""
store
=
self
.
_get_modulestore_for_courseid
(
asset_key
.
course_key
)
return
store
.
set_asset_metadata_attrs
(
asset_key
,
attr_dict
,
user_id
)
@strip_key
def
get_parent_location
(
self
,
location
,
**
kwargs
):
"""
...
...
common/lib/xmodule/xmodule/modulestore/mongo/base.py
View file @
5ffc6ac2
...
...
@@ -24,6 +24,7 @@ from fs.osfs import OSFS
from
path
import
path
from
datetime
import
datetime
from
pytz
import
UTC
from
contracts
import
contract
,
new_contract
from
importlib
import
import_module
from
xmodule.errortracker
import
null_error_tracker
,
exc_info_to_str
...
...
@@ -41,12 +42,18 @@ from xmodule.modulestore.inheritance import InheritanceMixin, inherit_metadata,
from
xblock.core
import
XBlock
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locator
import
CourseLocator
from
opaque_keys.edx.keys
import
UsageKey
,
CourseKey
from
opaque_keys.edx.keys
import
UsageKey
,
CourseKey
,
AssetKey
from
xmodule.exceptions
import
HeartbeatFailure
from
xmodule.modulestore.edit_info
import
EditInfoRuntimeMixin
from
xmodule.assetstore
import
AssetMetadata
,
AssetThumbnailMetadata
log
=
logging
.
getLogger
(
__name__
)
new_contract
(
'CourseKey'
,
CourseKey
)
new_contract
(
'AssetKey'
,
AssetKey
)
new_contract
(
'AssetMetadata'
,
AssetMetadata
)
new_contract
(
'AssetThumbnailMetadata'
,
AssetThumbnailMetadata
)
# sort order that returns DRAFT items first
SORT_REVISION_FAVOR_DRAFT
=
(
'_id.revision'
,
pymongo
.
DESCENDING
)
...
...
@@ -195,7 +202,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
category
=
json_data
[
'location'
][
'category'
]
class_
=
self
.
load_block_type
(
category
)
definition
=
json_data
.
get
(
'definition'
,
{})
metadata
=
json_data
.
get
(
'metadata'
,
{})
for
old_name
,
new_name
in
getattr
(
class_
,
'metadata_translations'
,
{})
.
items
():
...
...
@@ -443,7 +449,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
super
(
MongoModuleStore
,
self
)
.
__init__
(
contentstore
=
contentstore
,
**
kwargs
)
def
do_connection
(
db
,
collection
,
host
,
port
=
27017
,
tz_aware
=
True
,
user
=
None
,
password
=
None
,
**
kwargs
db
,
collection
,
host
,
port
=
27017
,
tz_aware
=
True
,
user
=
None
,
password
=
None
,
asset_collection
=
None
,
**
kwargs
):
"""
Create & open the connection, authenticate, and provide pointers to the collection
...
...
@@ -460,6 +466,11 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
)
self
.
collection
=
self
.
database
[
collection
]
# Collection which stores asset metadata.
self
.
asset_collection
=
None
if
asset_collection
is
not
None
:
self
.
asset_collection
=
self
.
database
[
asset_collection
]
if
user
is
not
None
and
password
is
not
None
:
self
.
database
.
authenticate
(
user
,
password
)
...
...
@@ -1436,6 +1447,147 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
field_data
=
KvsFieldData
(
kvs
)
return
field_data
def
_find_course_assets
(
self
,
course_key
):
"""
Internal; finds (or creates) course asset info about all assets for a particular course
Arguments:
course_key (CourseKey): course identifier
Returns:
Asset info for the course
"""
if
self
.
asset_collection
is
None
:
return
None
# Using the course_key, find or insert the course asset metadata document.
# A single document exists per course to store the course asset metadata.
course_assets
=
self
.
asset_collection
.
find_one
(
{
'course_id'
:
unicode
(
course_key
)},
fields
=
(
'course_id'
,
'storage'
,
'assets'
,
'thumbnails'
)
)
if
course_assets
is
None
:
# Not found, so create.
course_assets
=
{
'course_id'
:
unicode
(
course_key
),
'storage'
:
'FILLMEIN-TMP'
,
'assets'
:
[],
'thumbnails'
:
[]}
course_assets
[
'_id'
]
=
self
.
asset_collection
.
insert
(
course_assets
)
return
course_assets
@contract
(
course_key
=
'CourseKey'
,
asset_metadata
=
'AssetMetadata | AssetThumbnailMetadata'
,
user_id
=
'str | unicode'
)
def
_save_asset_info
(
self
,
course_key
,
asset_metadata
,
user_id
,
thumbnail
=
False
):
"""
Saves the info for a particular course's asset/thumbnail.
Arguments:
course_key (CourseKey): course identifier
asset_metadata (AssetMetadata/AssetThumbnailMetadata): data about the course asset/thumbnail
thumbnail (bool): True if saving thumbnail metadata, False if saving asset metadata
Returns:
True if info save was successful, else False
"""
if
self
.
asset_collection
is
None
:
return
False
course_assets
,
asset_idx
=
self
.
_find_course_asset
(
course_key
,
asset_metadata
.
asset_id
.
path
,
thumbnail
)
info
=
'thumbnails'
if
thumbnail
else
'assets'
all_assets
=
course_assets
[
info
]
# Set the edited information for assets only - not thumbnails.
if
not
thumbnail
:
asset_metadata
.
update
({
'edited_by'
:
user_id
,
'edited_on'
:
datetime
.
now
(
UTC
)})
# Translate metadata to Mongo format.
metadata_to_insert
=
asset_metadata
.
to_mongo
()
if
asset_idx
is
None
:
# Append new metadata.
# Future optimization: Insert in order & binary search to retrieve.
all_assets
.
append
(
metadata_to_insert
)
else
:
# Replace existing metadata.
all_assets
[
asset_idx
]
=
metadata_to_insert
# Update the document.
self
.
asset_collection
.
update
({
'_id'
:
course_assets
[
'_id'
]},
{
'$set'
:
{
info
:
all_assets
}})
return
True
@contract
(
asset_key
=
'AssetKey'
,
attr_dict
=
dict
)
def
set_asset_metadata_attrs
(
self
,
asset_key
,
attr_dict
,
user_id
):
"""
Add/set the given dict of attrs on the asset at the given location. Value can be any type which pymongo accepts.
Arguments:
asset_key (AssetKey): asset identifier
attr_dict (dict): attribute: value pairs to set
Raises:
ItemNotFoundError if no such item exists
AttributeError is attr is one of the build in attrs.
"""
if
self
.
asset_collection
is
None
:
return
course_assets
,
asset_idx
=
self
.
_find_course_asset
(
asset_key
.
course_key
,
asset_key
.
path
)
if
asset_idx
is
None
:
raise
ItemNotFoundError
(
asset_key
)
# Form an AssetMetadata.
all_assets
=
course_assets
[
'assets'
]
md
=
AssetMetadata
(
asset_key
,
asset_key
.
path
)
md
.
from_mongo
(
all_assets
[
asset_idx
])
md
.
update
(
attr_dict
)
md
.
update
({
'edited_by'
:
user_id
,
'edited_on'
:
datetime
.
now
(
UTC
)})
# Generate a Mongo doc from the metadata and update the course asset info.
all_assets
[
asset_idx
]
=
md
.
to_mongo
()
self
.
asset_collection
.
update
({
'_id'
:
course_assets
[
'_id'
]},
{
"$set"
:
{
'assets'
:
all_assets
}})
@contract
(
asset_key
=
'AssetKey'
)
def
_delete_asset_data
(
self
,
asset_key
,
thumbnail
=
False
):
"""
Internal; deletes a single asset's metadata -or- thumbnail.
Arguments:
asset_key (AssetKey): key containing original asset/thumbnail filename
thumbnail: True if thumbnail deletion, False if asset metadata deletion
Returns:
Number of asset metadata/thumbnail entries deleted (0 or 1)
"""
if
self
.
asset_collection
is
None
:
return
0
course_assets
,
asset_idx
=
self
.
_find_course_asset
(
asset_key
.
course_key
,
asset_key
.
path
,
get_thumbnail
=
thumbnail
)
if
asset_idx
is
None
:
return
0
info
=
'thumbnails'
if
thumbnail
else
'assets'
all_asset_info
=
course_assets
[
info
]
all_asset_info
.
pop
(
asset_idx
)
# Update the document.
self
.
asset_collection
.
update
({
'_id'
:
course_assets
[
'_id'
]},
{
'$set'
:
{
info
:
all_asset_info
}})
return
1
@contract
(
course_key
=
'CourseKey'
)
def
delete_all_asset_metadata
(
self
,
course_key
):
"""
Delete all of the assets which use this course_key as an identifier.
Arguments:
course_key (CourseKey): course_identifier
"""
if
self
.
asset_collection
is
None
:
return
# Using the course_id, find the course asset metadata document.
# A single document exists per course to store the course asset metadata.
course_assets
=
self
.
_find_course_assets
(
course_key
)
self
.
asset_collection
.
remove
(
course_assets
[
'_id'
])
def
heartbeat
(
self
):
"""
Check that the db is reachable.
...
...
common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py
View file @
5ffc6ac2
...
...
@@ -99,7 +99,7 @@ class MongoConnection(object):
Segregation of pymongo functions from the data modeling mechanisms for split modulestore.
"""
def
__init__
(
self
,
db
,
collection
,
host
,
port
=
27017
,
tz_aware
=
True
,
user
=
None
,
password
=
None
,
**
kwargs
self
,
db
,
collection
,
host
,
port
=
27017
,
tz_aware
=
True
,
user
=
None
,
password
=
None
,
asset_collection
=
None
,
**
kwargs
):
"""
Create & open the connection, authenticate, and provide pointers to the collections
...
...
@@ -114,6 +114,10 @@ class MongoConnection(object):
db
)
# Remove when adding official Split support for asset metadata storage.
if
asset_collection
:
pass
if
user
is
not
None
and
password
is
not
None
:
self
.
database
.
authenticate
(
user
,
password
)
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
View file @
5ffc6ac2
...
...
@@ -47,6 +47,7 @@ class TestMixedModuleStore(CourseComparisonTest):
PORT
=
MONGO_PORT_NUM
DB
=
'test_mongo_
%
s'
%
uuid4
()
.
hex
[:
5
]
COLLECTION
=
'modulestore'
ASSET_COLLECTION
=
'assetstore'
FS_ROOT
=
DATA_DIR
DEFAULT_CLASS
=
'xmodule.raw_module.RawDescriptor'
RENDER_TEMPLATE
=
lambda
t_n
,
d
,
ctx
=
None
,
nsp
=
'main'
:
''
...
...
@@ -67,6 +68,7 @@ class TestMixedModuleStore(CourseComparisonTest):
'port'
:
PORT
,
'db'
:
DB
,
'collection'
:
COLLECTION
,
'asset_collection'
:
ASSET_COLLECTION
,
}
OPTIONS
=
{
'mappings'
:
{
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
View file @
5ffc6ac2
...
...
@@ -14,6 +14,7 @@ from datetime import datetime
from
pytz
import
UTC
import
unittest
from
xblock.core
import
XBlock
from
ddt
import
ddt
,
data
from
xblock.fields
import
Scope
,
Reference
,
ReferenceList
,
ReferenceValueDict
from
xblock.runtime
import
KeyValueStore
...
...
@@ -30,6 +31,7 @@ from opaque_keys.edx.keys import UsageKey
from
xmodule.modulestore.xml_exporter
import
export_to_xml
from
xmodule.modulestore.xml_importer
import
import_from_xml
,
perform_xlint
from
xmodule.contentstore.mongo
import
MongoContentStore
from
xmodule.assetstore
import
AssetMetadata
,
AssetThumbnailMetadata
from
nose.tools
import
assert_in
from
xmodule.exceptions
import
NotFoundError
...
...
@@ -45,6 +47,7 @@ HOST = MONGO_HOST
PORT
=
MONGO_PORT_NUM
DB
=
'test_mongo_
%
s'
%
uuid4
()
.
hex
[:
5
]
COLLECTION
=
'modulestore'
ASSET_COLLECTION
=
'assetstore'
FS_ROOT
=
DATA_DIR
# TODO (vshnayder): will need a real fs_root for testing load_item
DEFAULT_CLASS
=
'xmodule.raw_module.RawDescriptor'
RENDER_TEMPLATE
=
lambda
t_n
,
d
,
ctx
=
None
,
nsp
=
'main'
:
''
...
...
@@ -60,8 +63,10 @@ class ReferenceTestXBlock(XBlock, XModuleMixin):
reference_dict
=
ReferenceValueDict
(
scope
=
Scope
.
settings
)
class
TestMongoModuleStore
(
unittest
.
TestCase
):
'''Tests!'''
class
TestMongoModuleStoreBase
(
unittest
.
TestCase
):
'''
Basic setup for all tests
'''
# Explicitly list the courses to load (don't want the big one)
courses
=
[
'toy'
,
'simple'
,
'simple_with_draft'
,
'test_unicode'
]
...
...
@@ -88,6 +93,13 @@ class TestMongoModuleStore(unittest.TestCase):
cls
.
connection
.
close
()
@classmethod
def
add_asset_collection
(
cls
,
doc_store_config
):
"""
No asset collection.
"""
pass
@classmethod
def
initdb
(
cls
):
# connect to the db
doc_store_config
=
{
...
...
@@ -95,7 +107,10 @@ class TestMongoModuleStore(unittest.TestCase):
'port'
:
PORT
,
'db'
:
DB
,
'collection'
:
COLLECTION
,
#'asset_collection': ASSET_COLLECTION,
}
cls
.
add_asset_collection
(
doc_store_config
)
# since MongoModuleStore and MongoContentStore are basically assumed to be together, create this class
# as well
content_store
=
MongoContentStore
(
HOST
,
DB
,
port
=
PORT
)
...
...
@@ -136,14 +151,33 @@ class TestMongoModuleStore(unittest.TestCase):
# Destroy the test db.
connection
.
drop_database
(
DB
)
def
setUp
(
self
):
# make a copy for convenience
self
.
connection
=
TestMongoModuleStore
.
connection
self
.
dummy_user
=
ModuleStoreEnum
.
UserID
.
test
@classmethod
def
setUp
(
cls
):
cls
.
dummy_user
=
ModuleStoreEnum
.
UserID
.
test
def
tearDown
(
self
):
@classmethod
def
tearDown
(
cls
):
pass
class
TestMongoModuleStore
(
TestMongoModuleStoreBase
):
'''Module store tests'''
@classmethod
def
add_asset_collection
(
cls
,
doc_store_config
):
"""
No asset collection - it's not used in the tests below.
"""
pass
@classmethod
def
setupClass
(
cls
):
super
(
TestMongoModuleStore
,
cls
)
.
setupClass
()
@classmethod
def
teardownClass
(
cls
):
super
(
TestMongoModuleStore
,
cls
)
.
teardownClass
()
def
test_init
(
self
):
'''Make sure the db loads'''
ids
=
list
(
self
.
connection
[
DB
][
COLLECTION
]
.
find
({},
{
'_id'
:
True
}))
...
...
@@ -233,7 +267,6 @@ class TestMongoModuleStore(unittest.TestCase):
self
.
draft_store
.
get_item
(
Location
(
'edX'
,
'test_unicode'
,
'2012_Fall'
,
'chapter'
,
'Overview'
)),
)
def
test_find_one
(
self
):
assert_not_none
(
self
.
draft_store
.
_find_one
(
Location
(
'edX'
,
'toy'
,
'2012_Fall'
,
'course'
,
'2012_Fall'
)),
...
...
@@ -632,6 +665,363 @@ class TestMongoModuleStore(unittest.TestCase):
shutil
.
rmtree
(
root_dir
)
@ddt
class
TestMongoAssetMetadataStorage
(
TestMongoModuleStore
):
"""
Tests for storing/querying course asset metadata from Mongo storage.
"""
def
_make_asset_metadata
(
self
,
asset_loc
):
"""
Make a single test asset metadata.
"""
return
AssetMetadata
(
asset_loc
,
internal_name
=
'EKMND332DDBK'
,
basename
=
'pictures/historical'
,
contenttype
=
'image/jpeg'
,
locked
=
False
,
md5
=
'77631ca4f0e08419b70726a447333ab6'
,
curr_version
=
'v1.0'
,
prev_version
=
'v0.95'
)
def
_make_asset_thumbnail_metadata
(
self
,
asset_key
):
"""
Make a single test asset thumbnail metadata.
"""
return
AssetThumbnailMetadata
(
asset_key
,
internal_name
=
'ABC39XJUDN2'
)
@classmethod
def
add_asset_collection
(
cls
,
doc_store_config
):
"""
Valid asset collection.
"""
doc_store_config
[
'asset_collection'
]
=
ASSET_COLLECTION
@classmethod
def
setupClass
(
cls
):
super
(
TestMongoAssetMetadataStorage
,
cls
)
.
setupClass
()
@classmethod
def
teardownClass
(
cls
):
super
(
TestMongoAssetMetadataStorage
,
cls
)
.
teardownClass
()
def
setup_assets
(
self
):
"""
Setup assets.
"""
asset_fields
=
(
'filename'
,
'internal_name'
,
'basename'
,
'locked'
,
'curr_version'
,
'prev_version'
)
asset1_vals
=
(
'pic1.jpg'
,
'EKMND332DDBK'
,
'pix/archive'
,
False
,
'14'
,
'13'
)
asset2_vals
=
(
'shout.ogg'
,
'KFMDONSKF39K'
,
'sounds'
,
True
,
'1'
,
None
)
asset3_vals
=
(
'code.tgz'
,
'ZZB2333YBDMW'
,
'exercises/14'
,
False
,
'AB'
,
'AA'
)
asset4_vals
=
(
'dog.png'
,
'PUPY4242X'
,
'pictures/animals'
,
True
,
'5'
,
'4'
)
asset5_vals
=
(
'not_here.txt'
,
'JJJCCC747'
,
'/dev/null'
,
False
,
'50'
,
'49'
)
asset1
=
dict
(
zip
(
asset_fields
[
1
:],
asset1_vals
[
1
:]))
asset2
=
dict
(
zip
(
asset_fields
[
1
:],
asset2_vals
[
1
:]))
asset3
=
dict
(
zip
(
asset_fields
[
1
:],
asset3_vals
[
1
:]))
asset4
=
dict
(
zip
(
asset_fields
[
1
:],
asset4_vals
[
1
:]))
non_existent_asset
=
dict
(
zip
(
asset_fields
[
1
:],
asset5_vals
[
1
:]))
# Asset6 and thumbnail6 have equivalent information on purpose.
asset6_vals
=
(
'asset.txt'
,
'JJJCCC747858'
,
'/dev/null'
,
False
,
'50'
,
'49'
)
asset6
=
dict
(
zip
(
asset_fields
[
1
:],
asset6_vals
[
1
:]))
asset1_key
=
self
.
course1
.
id
.
make_asset_key
(
'asset'
,
asset1_vals
[
0
])
asset2_key
=
self
.
course1
.
id
.
make_asset_key
(
'asset'
,
asset2_vals
[
0
])
asset3_key
=
self
.
course2
.
id
.
make_asset_key
(
'asset'
,
asset3_vals
[
0
])
asset4_key
=
self
.
course2
.
id
.
make_asset_key
(
'asset'
,
asset4_vals
[
0
])
asset5_key
=
self
.
course2
.
id
.
make_asset_key
(
'asset'
,
asset5_vals
[
0
])
asset6_key
=
self
.
course2
.
id
.
make_asset_key
(
'asset'
,
asset6_vals
[
0
])
asset1_md
=
AssetMetadata
(
asset1_key
,
**
asset1
)
asset2_md
=
AssetMetadata
(
asset2_key
,
**
asset2
)
asset3_md
=
AssetMetadata
(
asset3_key
,
**
asset3
)
asset4_md
=
AssetMetadata
(
asset4_key
,
**
asset4
)
asset5_md
=
AssetMetadata
(
asset5_key
,
**
non_existent_asset
)
asset6_md
=
AssetMetadata
(
asset6_key
,
**
asset6
)
editing_user
=
'Oliver Twist'
self
.
assertTrue
(
self
.
draft_store
.
save_asset_metadata
(
self
.
course1
.
id
,
asset1_md
,
editing_user
))
self
.
assertTrue
(
self
.
draft_store
.
save_asset_metadata
(
self
.
course1
.
id
,
asset2_md
,
editing_user
))
self
.
assertTrue
(
self
.
draft_store
.
save_asset_metadata
(
self
.
course2
.
id
,
asset3_md
,
editing_user
))
self
.
assertTrue
(
self
.
draft_store
.
save_asset_metadata
(
self
.
course2
.
id
,
asset4_md
,
editing_user
))
# asset5 and asset6 are not saved on purpose!
return
(
asset1_md
,
asset2_md
,
asset3_md
,
asset4_md
,
asset5_md
,
asset6_md
)
def
setup_thumbnails
(
self
):
"""
Setup thumbs.
"""
thumbnail_fields
=
(
'filename'
,
'internal_name'
)
thumbnail1_vals
=
(
'cat_thumb.jpg'
,
'XYXYXYXYXYXY'
)
thumbnail2_vals
=
(
'kitten_thumb.jpg'
,
'123ABC123ABC'
)
thumbnail3_vals
=
(
'puppy_thumb.jpg'
,
'ADAM12ADAM12'
)
thumbnail4_vals
=
(
'meerkat_thumb.jpg'
,
'CHIPSPONCH14'
)
thumbnail5_vals
=
(
'corgi_thumb.jpg'
,
'RON8LDXFFFF10'
)
thumbnail1
=
dict
(
zip
(
thumbnail_fields
[
1
:],
thumbnail1_vals
[
1
:]))
thumbnail2
=
dict
(
zip
(
thumbnail_fields
[
1
:],
thumbnail2_vals
[
1
:]))
thumbnail3
=
dict
(
zip
(
thumbnail_fields
[
1
:],
thumbnail3_vals
[
1
:]))
thumbnail4
=
dict
(
zip
(
thumbnail_fields
[
1
:],
thumbnail4_vals
[
1
:]))
non_existent_thumbnail
=
dict
(
zip
(
thumbnail_fields
[
1
:],
thumbnail5_vals
[
1
:]))
# Asset6 and thumbnail6 have equivalent information on purpose.
thumbnail6_vals
=
(
'asset.txt'
,
'JJJCCC747858'
)
thumbnail6
=
dict
(
zip
(
thumbnail_fields
[
1
:],
thumbnail6_vals
[
1
:]))
thumb1_key
=
self
.
course1
.
id
.
make_asset_key
(
'thumbnail'
,
thumbnail1_vals
[
0
])
thumb2_key
=
self
.
course1
.
id
.
make_asset_key
(
'thumbnail'
,
thumbnail2_vals
[
0
])
thumb3_key
=
self
.
course2
.
id
.
make_asset_key
(
'thumbnail'
,
thumbnail3_vals
[
0
])
thumb4_key
=
self
.
course2
.
id
.
make_asset_key
(
'thumbnail'
,
thumbnail4_vals
[
0
])
thumb5_key
=
self
.
course2
.
id
.
make_asset_key
(
'thumbnail'
,
thumbnail5_vals
[
0
])
thumb6_key
=
self
.
course2
.
id
.
make_asset_key
(
'thumbnail'
,
thumbnail6_vals
[
0
])
thumb1_md
=
AssetThumbnailMetadata
(
thumb1_key
,
**
thumbnail1
)
thumb2_md
=
AssetThumbnailMetadata
(
thumb2_key
,
**
thumbnail2
)
thumb3_md
=
AssetThumbnailMetadata
(
thumb3_key
,
**
thumbnail3
)
thumb4_md
=
AssetThumbnailMetadata
(
thumb4_key
,
**
thumbnail4
)
thumb5_md
=
AssetThumbnailMetadata
(
thumb5_key
,
**
non_existent_thumbnail
)
thumb6_md
=
AssetThumbnailMetadata
(
thumb6_key
,
**
thumbnail6
)
self
.
assertTrue
(
self
.
draft_store
.
save_asset_thumbnail_metadata
(
self
.
course1
.
id
,
thumb1_md
))
self
.
assertTrue
(
self
.
draft_store
.
save_asset_thumbnail_metadata
(
self
.
course1
.
id
,
thumb2_md
))
self
.
assertTrue
(
self
.
draft_store
.
save_asset_thumbnail_metadata
(
self
.
course2
.
id
,
thumb3_md
))
self
.
assertTrue
(
self
.
draft_store
.
save_asset_thumbnail_metadata
(
self
.
course2
.
id
,
thumb4_md
))
# thumb5 and thumb6 are not saved on purpose!
return
(
thumb1_md
,
thumb2_md
,
thumb3_md
,
thumb4_md
,
thumb5_md
,
thumb6_md
)
def
setUp
(
self
):
"""
Set up a quantity of test asset metadata for testing purposes.
"""
super
(
TestMongoAssetMetadataStorage
,
self
)
.
setUp
()
courses
=
self
.
draft_store
.
get_courses
()
self
.
course1
=
courses
[
0
]
self
.
course2
=
courses
[
1
]
(
self
.
asset1_md
,
self
.
asset2_md
,
self
.
asset3_md
,
self
.
asset4_md
,
self
.
asset5_md
,
self
.
asset6_md
)
=
self
.
setup_assets
()
(
self
.
thumb1_md
,
self
.
thumb2_md
,
self
.
thumb3_md
,
self
.
thumb4_md
,
self
.
thumb5_md
,
self
.
thumb6_md
)
=
self
.
setup_thumbnails
()
def
tearDown
(
self
):
self
.
draft_store
.
delete_all_asset_metadata
(
self
.
course1
.
id
)
self
.
draft_store
.
delete_all_asset_metadata
(
self
.
course2
.
id
)
def
test_save_one_and_confirm
(
self
):
courses
=
self
.
draft_store
.
get_courses
()
course
=
courses
[
0
]
asset_filename
=
'burnside.jpg'
new_asset_loc
=
course
.
id
.
make_asset_key
(
'asset'
,
asset_filename
)
# Confirm that the asset's metadata is not present.
self
.
assertIsNone
(
self
.
draft_store
.
find_asset_metadata
(
new_asset_loc
))
# Save the asset's metadata.
new_asset_md
=
self
.
_make_asset_metadata
(
new_asset_loc
)
self
.
assertTrue
(
self
.
draft_store
.
save_asset_metadata
(
course
.
id
,
new_asset_md
,
'John Doe'
))
# Find the asset's metadata and confirm it's the same.
found_asset_md
=
self
.
draft_store
.
find_asset_metadata
(
new_asset_loc
)
self
.
assertIsNotNone
(
found_asset_md
)
self
.
assertEquals
(
new_asset_md
.
asset_id
,
found_asset_md
.
asset_id
)
# Confirm that only two setup plus one asset's metadata exists.
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
)),
3
)
# Delete all metadata and confirm it's gone.
self
.
draft_store
.
delete_all_asset_metadata
(
course
.
id
)
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
)),
0
)
def
test_delete_all_without_creation
(
self
):
courses
=
self
.
draft_store
.
get_courses
()
course
=
courses
[
0
]
# Confirm that only setup asset metadata exists.
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
)),
2
)
# Now delete the metadata.
self
.
draft_store
.
delete_all_asset_metadata
(
course
.
id
)
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
)),
0
)
# Now delete the non-existent metadata.
self
.
draft_store
.
delete_all_asset_metadata
(
course
.
id
)
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
)),
0
)
def
test_save_many_and_delete_one
(
self
):
# Make sure there's two assets.
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
self
.
course1
.
id
)),
2
)
# Delete one of the assets.
self
.
assertEquals
(
self
.
draft_store
.
delete_asset_metadata
(
self
.
asset1_md
.
asset_id
),
1
)
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
self
.
course1
.
id
)),
1
)
# Attempt to delete an asset that doesn't exist.
self
.
assertEquals
(
self
.
draft_store
.
delete_asset_metadata
(
self
.
asset5_md
.
asset_id
),
0
)
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
self
.
course1
.
id
)),
1
)
def
test_find_existing_and_non_existing_assets
(
self
):
# Find existing asset metadata.
asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
asset_md
)
# Find non-existent asset metadata.
asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset5_md
.
asset_id
)
self
.
assertIsNone
(
asset_md
)
def
test_add_same_asset_twice
(
self
):
courses
=
self
.
draft_store
.
get_courses
()
course
=
courses
[
0
]
asset_filename
=
'burnside.jpg'
new_asset_loc
=
course
.
id
.
make_asset_key
(
'asset'
,
asset_filename
)
new_asset_md
=
self
.
_make_asset_metadata
(
new_asset_loc
)
# Only the setup stuff here?
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
)),
2
)
# Add asset metadata.
self
.
assertTrue
(
self
.
draft_store
.
save_asset_metadata
(
course
.
id
,
new_asset_md
,
"John Do"
))
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
)),
3
)
# Add *the same* asset metadata.
self
.
assertTrue
(
self
.
draft_store
.
save_asset_metadata
(
course
.
id
,
new_asset_md
,
"John Dont"
))
# Still one here?
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
)),
3
)
self
.
draft_store
.
delete_all_asset_metadata
(
course
.
id
)
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
)),
0
)
def
test_lock_unlock_assets
(
self
):
# Find a course asset and check its locked status.
asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
asset_md
)
locked_state
=
asset_md
.
locked
# Flip the course asset's locked status.
self
.
draft_store
.
set_asset_metadata_attr
(
self
.
asset1_md
.
asset_id
,
"locked"
,
not
locked_state
,
'John Doe'
)
# Find the same course and check its locked status.
updated_asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
updated_asset_md
)
self
.
assertEquals
(
updated_asset_md
.
locked
,
not
locked_state
)
# Now flip it back.
self
.
draft_store
.
set_asset_metadata_attr
(
self
.
asset1_md
.
asset_id
,
"locked"
,
locked_state
,
'John Doe'
)
reupdated_asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
reupdated_asset_md
)
self
.
assertEquals
(
reupdated_asset_md
.
locked
,
locked_state
)
ALLOWED_ATTRS
=
(
(
'basename'
,
'/new/path'
),
(
'internal_name'
,
'new_filename.txt'
),
(
'locked'
,
True
),
(
'contenttype'
,
'image/png'
),
(
'md5'
,
'5346682d948cc3f683635b6918f9b3d0'
),
(
'curr_version'
,
'v1.01'
),
(
'prev_version'
,
'v1.0'
),
(
'edited_by'
,
'Mork'
),
(
'edited_on'
,
datetime
(
1969
,
1
,
1
,
tzinfo
=
UTC
)),
)
DISALLOWED_ATTRS
=
(
(
'asset_id'
,
'IAmBogus'
),
)
UNKNOWN_ATTRS
=
(
(
'lunch_order'
,
'burger_and_fries'
),
(
'villain'
,
'Khan'
)
)
@data
(
*
ALLOWED_ATTRS
)
def
test_set_all_attrs
(
self
,
attr_pair
):
# Find a course asset.
asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
asset_md
)
# Set the course asset's attr.
editing_user
=
'user_who_edited'
self
.
draft_store
.
set_asset_metadata_attr
(
self
.
asset1_md
.
asset_id
,
*
attr_pair
,
user_id
=
editing_user
)
# Find the same course asset and check its changed attr.
updated_asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
updated_asset_md
)
self
.
assertIsNotNone
(
getattr
(
updated_asset_md
,
attr_pair
[
0
],
None
))
if
attr_pair
[
0
]
==
'edited_by'
:
# No matter what the edited_by attr_pair is, it gets over-ridden by the passed-in user_id.
self
.
assertEquals
(
getattr
(
updated_asset_md
,
attr_pair
[
0
],
None
),
editing_user
)
elif
attr_pair
[
0
]
==
'edited_on'
:
# edited_on is also over-ridden to be the time of update.
pass
else
:
self
.
assertEquals
(
getattr
(
updated_asset_md
,
attr_pair
[
0
],
None
),
attr_pair
[
1
])
@data
(
*
DISALLOWED_ATTRS
)
def
test_set_disallowed_attrs
(
self
,
attr_pair
):
# Find a course asset.
asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
asset_md
)
original_attr_val
=
getattr
(
asset_md
,
attr_pair
[
0
])
# Set the course asset's attr.
self
.
draft_store
.
set_asset_metadata_attr
(
self
.
asset1_md
.
asset_id
,
*
attr_pair
,
user_id
=
'John Doe'
)
# Find the same course and check its changed attr.
updated_asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
updated_asset_md
)
self
.
assertIsNotNone
(
getattr
(
updated_asset_md
,
attr_pair
[
0
],
None
))
# Make sure that the attr is unchanged from its original value.
self
.
assertEquals
(
getattr
(
updated_asset_md
,
attr_pair
[
0
],
None
),
original_attr_val
)
@data
(
*
UNKNOWN_ATTRS
)
def
test_set_unknown_attrs
(
self
,
attr_pair
):
# Find a course asset.
asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
asset_md
)
# Set the course asset's attr.
self
.
draft_store
.
set_asset_metadata_attr
(
self
.
asset1_md
.
asset_id
,
*
attr_pair
,
user_id
=
'John Smith'
)
# Find the same course and check its changed attr.
updated_asset_md
=
self
.
draft_store
.
find_asset_metadata
(
self
.
asset1_md
.
asset_id
)
self
.
assertIsNotNone
(
updated_asset_md
)
# Make sure the unknown field was *not* added.
with
self
.
assertRaises
(
AttributeError
):
self
.
assertEquals
(
getattr
(
updated_asset_md
,
attr_pair
[
0
]),
attr_pair
[
1
])
def
test_save_one_thumbnail_and_delete_one_thumbnail
(
self
):
thumbnail_filename
=
'burn_thumb.jpg'
asset_key
=
self
.
course1
.
id
.
make_asset_key
(
'thumbnail'
,
thumbnail_filename
)
new_asset_thumbnail
=
self
.
_make_asset_thumbnail_metadata
(
asset_key
)
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_thumbnail_metadata
(
self
.
course1
.
id
)),
2
)
self
.
assertTrue
(
self
.
draft_store
.
save_asset_thumbnail_metadata
(
self
.
course1
.
id
,
new_asset_thumbnail
))
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_thumbnail_metadata
(
self
.
course1
.
id
)),
3
)
self
.
assertEquals
(
self
.
draft_store
.
delete_asset_thumbnail_metadata
(
asset_key
),
1
)
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_thumbnail_metadata
(
self
.
course1
.
id
)),
2
)
def
test_find_thumbnail
(
self
):
self
.
assertIsNotNone
(
self
.
draft_store
.
find_asset_thumbnail_metadata
(
self
.
thumb1_md
.
asset_id
))
self
.
assertIsNone
(
self
.
draft_store
.
find_asset_thumbnail_metadata
(
self
.
thumb5_md
.
asset_id
))
def
test_delete_all_thumbnails
(
self
):
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_thumbnail_metadata
(
self
.
course1
.
id
)),
2
)
self
.
draft_store
.
delete_all_asset_metadata
(
self
.
course1
.
id
)
self
.
assertEquals
(
len
(
self
.
draft_store
.
get_all_asset_thumbnail_metadata
(
self
.
course1
.
id
)),
0
)
def
test_asset_object_equivalence
(
self
):
# Assets are only equivalent to themselves.
self
.
assertTrue
(
self
.
asset6_md
!=
self
.
thumb6_md
)
self
.
assertEquals
(
self
.
asset1_md
,
self
.
asset1_md
)
def
test_get_all_assets_with_paging
(
self
):
pass
def
test_copy_all_assets
(
self
):
pass
class
TestMongoModuleStoreWithNoAssetCollection
(
TestMongoModuleStore
):
'''
Tests a situation where no asset_collection is specified.
'''
@classmethod
def
add_asset_collection
(
cls
,
doc_store_config
):
"""
No asset collection.
"""
pass
@classmethod
def
setupClass
(
cls
):
super
(
TestMongoModuleStoreWithNoAssetCollection
,
cls
)
.
setupClass
()
@classmethod
def
teardownClass
(
cls
):
super
(
TestMongoModuleStoreWithNoAssetCollection
,
cls
)
.
teardownClass
()
def
test_no_asset_collection
(
self
):
courses
=
self
.
draft_store
.
get_courses
()
course
=
courses
[
0
]
# Confirm that no asset collection means no asset metadata.
self
.
assertEquals
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
),
None
)
# Now delete the non-existent asset metadata.
self
.
draft_store
.
delete_all_asset_metadata
(
course
.
id
)
# Should still be nothing.
self
.
assertEquals
(
self
.
draft_store
.
get_all_asset_metadata
(
course
.
id
),
None
)
class
TestMongoKeyValueStore
(
object
):
"""
Tests for MongoKeyValueStore.
...
...
lms/envs/common.py
View file @
5ffc6ac2
...
...
@@ -566,6 +566,7 @@ DOC_STORE_CONFIG = {
'host'
:
'localhost'
,
'db'
:
'xmodule'
,
'collection'
:
'modulestore'
,
'asset_collection'
:
'assetstore'
,
}
MODULESTORE
=
{
'default'
:
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment