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
29dcd88e
Commit
29dcd88e
authored
Nov 25, 2014
by
John Eskew
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement XML input/output for AssetMetadata.
Add tests which validate XML using XSD.
parent
6317347d
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
256 additions
and
39 deletions
+256
-39
common/lib/xmodule/xmodule/assetstore/__init__.py
+94
-17
common/lib/xmodule/xmodule/assetstore/tests/assets.xsd
+49
-0
common/lib/xmodule/xmodule/assetstore/tests/test_asset_xml.py
+84
-0
common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py
+29
-22
No files found.
common/lib/xmodule/xmodule/assetstore/__init__.py
View file @
29dcd88e
...
...
@@ -3,13 +3,19 @@ Classes representing asset metadata.
"""
from
datetime
import
datetime
import
dateutil.parser
import
pytz
import
json
from
contracts
import
contract
,
new_contract
from
opaque_keys.edx.keys
import
AssetKey
from
lxml
import
etree
new_contract
(
'AssetKey'
,
AssetKey
)
new_contract
(
'datetime'
,
datetime
)
new_contract
(
'basestring'
,
basestring
)
new_contract
(
'AssetElement'
,
lambda
x
:
isinstance
(
x
,
etree
.
_Element
)
and
x
.
tag
==
"asset"
)
# pylint: disable=protected-access, no-member
new_contract
(
'AssetsElement'
,
lambda
x
:
isinstance
(
x
,
etree
.
_Element
)
and
x
.
tag
==
"assets"
)
# pylint: disable=protected-access, no-member
class
AssetMetadata
(
object
):
...
...
@@ -19,8 +25,10 @@ class AssetMetadata(object):
"""
TOP_LEVEL_ATTRS
=
[
'basename'
,
'internal_name'
,
'locked'
,
'contenttype'
,
'thumbnail'
,
'fields'
]
EDIT_INFO_ATTRS
=
[
'curr_version'
,
'prev_version'
,
'edited_by'
,
'edited_on'
]
ALLOWED_ATTRS
=
TOP_LEVEL_ATTRS
+
EDIT_INFO_ATTRS
EDIT_INFO_ATTRS
=
[
'curr_version'
,
'prev_version'
,
'edited_by'
,
'edited_by_email'
,
'edited_on'
]
CREATE_INFO_ATTRS
=
[
'created_by'
,
'created_by_email'
,
'created_on'
]
ATTRS_ALLOWED_TO_UPDATE
=
TOP_LEVEL_ATTRS
+
EDIT_INFO_ATTRS
ALL_ATTRS
=
[
'asset_id'
]
+
ATTRS_ALLOWED_TO_UPDATE
+
CREATE_INFO_ATTRS
# Default type for AssetMetadata objects. A constant for convenience.
ASSET_TYPE
=
'asset'
...
...
@@ -30,15 +38,15 @@ class AssetMetadata(object):
locked
=
'bool|None'
,
contenttype
=
'basestring|None'
,
thumbnail
=
'basestring|None'
,
fields
=
'dict|None'
,
curr_version
=
'basestring|None'
,
prev_version
=
'basestring|None'
,
created_by
=
'int|None'
,
created_on
=
'datetime|None'
,
edited_by
=
'int|None'
,
edited_on
=
'datetime|None'
)
created_by
=
'int|None'
,
created_
by_email
=
'basestring|None'
,
created_
on
=
'datetime|None'
,
edited_by
=
'int|None'
,
edited_
by_email
=
'basestring|None'
,
edited_
on
=
'datetime|None'
)
def
__init__
(
self
,
asset_id
,
basename
=
None
,
internal_name
=
None
,
locked
=
None
,
contenttype
=
None
,
thumbnail
=
None
,
fields
=
None
,
curr_version
=
None
,
prev_version
=
None
,
created_by
=
None
,
created_on
=
None
,
edited_by
=
None
,
edited_on
=
None
,
created_by
=
None
,
created_
by_email
=
None
,
created_
on
=
None
,
edited_by
=
None
,
edited_
by_email
=
None
,
edited_
on
=
None
,
field_decorator
=
None
,):
"""
Construct a AssetMetadata object.
...
...
@@ -53,7 +61,11 @@ class AssetMetadata(object):
fields (dict): fields to save w/ the metadata
curr_version (str): Current version of the asset.
prev_version (str): Previous version of the asset.
edited_by (str): Username of last user to upload this asset.
created_by (int): User ID of initial user to upload this asset.
created_by_email (str): Email address of initial user to upload this asset.
created_on (datetime): Datetime of intial upload of this asset.
edited_by (int): User ID of last user to upload this asset.
edited_by_email (str): Email address of last user to upload this asset.
edited_on (datetime): Datetime of last upload of this asset.
field_decorator (function): used by strip_key to convert OpaqueKeys to the app's understanding.
Not saved.
...
...
@@ -68,9 +80,11 @@ class AssetMetadata(object):
self
.
prev_version
=
prev_version
now
=
datetime
.
now
(
pytz
.
utc
)
self
.
edited_by
=
edited_by
self
.
edited_by_email
=
edited_by_email
self
.
edited_on
=
edited_on
or
now
# created_by and created_on should only be set here.
# created_by
, created_by_email,
and created_on should only be set here.
self
.
created_by
=
created_by
self
.
created_by_email
=
created_by_email
self
.
created_on
=
created_on
or
now
self
.
fields
=
fields
or
{}
...
...
@@ -80,20 +94,20 @@ class AssetMetadata(object):
self
.
basename
,
self
.
internal_name
,
self
.
locked
,
self
.
contenttype
,
self
.
fields
,
self
.
curr_version
,
self
.
prev_version
,
self
.
edited_by
,
self
.
edi
ted_on
,
self
.
created_by
,
self
.
created_on
self
.
created_by
,
self
.
created_by_email
,
self
.
crea
ted_on
,
self
.
edited_by
,
self
.
edited_by_email
,
self
.
edited_on
,
))
def
update
(
self
,
attr_dict
):
"""
Set the attributes on the metadata. Any which are not in A
LLOWED_ATTRS
get put into
Set the attributes on the metadata. Any which are not in A
TTRS_ALLOWED_TO_UPDATE
get put into
fields.
Arguments:
attr_dict: Prop, val dictionary of all attributes to set.
"""
for
attr
,
val
in
attr_dict
.
iteritems
():
if
attr
in
self
.
A
LLOWED_ATTRS
:
if
attr
in
self
.
A
TTRS_ALLOWED_TO_UPDATE
:
setattr
(
self
,
attr
,
val
)
else
:
self
.
fields
[
attr
]
=
val
...
...
@@ -113,10 +127,12 @@ class AssetMetadata(object):
'edit_info'
:
{
'curr_version'
:
self
.
curr_version
,
'prev_version'
:
self
.
prev_version
,
'edited_by'
:
self
.
edited_by
,
'edited_on'
:
self
.
edited_on
,
'created_by'
:
self
.
created_by
,
'created_on'
:
self
.
created_on
'created_by_email'
:
self
.
created_by_email
,
'created_on'
:
self
.
created_on
,
'edited_by'
:
self
.
edited_by
,
'edited_by_email'
:
self
.
edited_by_email
,
'edited_on'
:
self
.
edited_on
}
}
...
...
@@ -137,7 +153,68 @@ class AssetMetadata(object):
self
.
fields
=
asset_doc
[
'fields'
]
self
.
curr_version
=
asset_doc
[
'edit_info'
][
'curr_version'
]
self
.
prev_version
=
asset_doc
[
'edit_info'
][
'prev_version'
]
self
.
edited_by
=
asset_doc
[
'edit_info'
][
'edited_by'
]
self
.
edited_on
=
asset_doc
[
'edit_info'
][
'edited_on'
]
self
.
created_by
=
asset_doc
[
'edit_info'
][
'created_by'
]
self
.
created_by_email
=
asset_doc
[
'edit_info'
][
'created_by_email'
]
self
.
created_on
=
asset_doc
[
'edit_info'
][
'created_on'
]
self
.
edited_by
=
asset_doc
[
'edit_info'
][
'edited_by'
]
self
.
edited_by_email
=
asset_doc
[
'edit_info'
][
'edited_by_email'
]
self
.
edited_on
=
asset_doc
[
'edit_info'
][
'edited_on'
]
@contract
(
node
=
'AssetElement'
)
def
from_xml
(
self
,
node
):
"""
Walk the etree XML node and fill in the asset metadata.
The node should be a top-level "asset" element.
"""
for
child
in
node
:
qname
=
etree
.
QName
(
child
)
tag
=
qname
.
localname
if
tag
in
self
.
ALL_ATTRS
:
value
=
child
.
text
if
tag
==
'asset_id'
:
# Locator.
value
=
AssetKey
.
from_string
(
value
)
elif
tag
==
'locked'
:
# Boolean.
value
=
True
if
value
==
"true"
else
False
elif
tag
in
(
'created_on'
,
'edited_on'
):
# ISO datetime.
value
=
dateutil
.
parser
.
parse
(
value
)
elif
tag
in
(
'created_by'
,
'edited_by'
):
# Integer representing user id.
value
=
int
(
value
)
elif
tag
==
'fields'
:
# Dictionary.
value
=
json
.
loads
(
value
)
elif
value
==
'None'
:
# None.
value
=
None
setattr
(
self
,
tag
,
value
)
@contract
(
node
=
'AssetElement'
)
def
to_xml
(
self
,
node
):
"""
Add the asset data as XML to the passed-in node.
The node should already be created as a top-level "asset" element.
"""
for
attr
in
self
.
ALL_ATTRS
:
child
=
etree
.
SubElement
(
node
,
attr
)
value
=
getattr
(
self
,
attr
)
if
isinstance
(
value
,
bool
):
value
=
"true"
if
value
else
"false"
elif
isinstance
(
value
,
datetime
):
value
=
value
.
isoformat
()
else
:
value
=
unicode
(
value
)
child
.
text
=
value
@staticmethod
@contract
(
node
=
'AssetsElement'
,
assets
=
list
)
def
add_all_assets_as_xml
(
node
,
assets
):
"""
Take a list of AssetMetadata objects. Add them all to the node.
The node should already be created as a top-level "assets" element.
"""
for
asset
in
assets
:
asset_node
=
etree
.
SubElement
(
node
,
"asset"
)
asset
.
to_xml
(
asset_node
)
common/lib/xmodule/xmodule/assetstore/tests/assets.xsd
0 → 100644
View file @
29dcd88e
<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema
xmlns:xs=
"http://www.w3.org/2001/XMLSchema"
>
<xs:element
name=
"assets"
type=
"assetListType"
/>
<xs:simpleType
name=
"stringType"
>
<xs:restriction
base=
"xs:string"
/>
</xs:simpleType>
<xs:simpleType
name=
"userIdType"
>
<xs:restriction
base=
"xs:nonNegativeInteger"
/>
</xs:simpleType>
<xs:simpleType
name=
"datetimeType"
>
<xs:restriction
base=
"xs:dateTime"
/>
</xs:simpleType>
<xs:simpleType
name=
"boolType"
>
<xs:restriction
base=
"xs:boolean"
/>
</xs:simpleType>
<xs:complexType
name=
"assetListType"
>
<xs:sequence>
<xs:element
name=
"asset"
type=
"assetType"
minOccurs=
"0"
maxOccurs=
"unbounded"
/>
</xs:sequence>
</xs:complexType>
<xs:complexType
name=
"assetType"
>
<xs:all>
<xs:element
name=
"asset_id"
type=
"stringType"
/>
<xs:element
name=
"contenttype"
type=
"stringType"
/>
<xs:element
name=
"basename"
type=
"stringType"
/>
<xs:element
name=
"internal_name"
type=
"stringType"
/>
<xs:element
name=
"locked"
type=
"boolType"
/>
<xs:element
name=
"thumbnail"
type=
"stringType"
minOccurs=
"0"
/>
<xs:element
name=
"created_on"
type=
"datetimeType"
/>
<xs:element
name=
"created_by"
type=
"userIdType"
/>
<xs:element
name=
"created_by_email"
type=
"stringType"
minOccurs=
"0"
/>
<xs:element
name=
"edited_on"
type=
"datetimeType"
/>
<xs:element
name=
"edited_by"
type=
"userIdType"
/>
<xs:element
name=
"edited_by_email"
type=
"stringType"
minOccurs=
"0"
/>
<xs:element
name=
"prev_version"
type=
"stringType"
/>
<xs:element
name=
"curr_version"
type=
"stringType"
/>
<xs:element
name=
"fields"
type=
"stringType"
minOccurs=
"0"
/>
</xs:all>
</xs:complexType>
</xs:schema>
\ No newline at end of file
common/lib/xmodule/xmodule/assetstore/tests/test_asset_xml.py
0 → 100644
View file @
29dcd88e
"""
Test for asset XML generation / parsing.
"""
from
path
import
path
from
lxml
import
etree
from
contracts
import
ContractNotRespected
import
unittest
from
opaque_keys.edx.locator
import
CourseLocator
from
xmodule.assetstore
import
AssetMetadata
from
xmodule.modulestore.tests.test_assetstore
import
AssetStoreTestData
class
TestAssetXml
(
unittest
.
TestCase
):
"""
Tests for storing/querying course asset metadata.
"""
def
setUp
(
self
):
super
(
TestAssetXml
,
self
)
.
setUp
()
xsd_filename
=
"assets.xsd"
self
.
course_id
=
CourseLocator
(
'org1'
,
'course1'
,
'run1'
)
self
.
course_assets
=
[]
for
asset
in
AssetStoreTestData
.
all_asset_data
:
asset_dict
=
dict
(
zip
(
AssetStoreTestData
.
asset_fields
[
1
:],
asset
[
1
:]))
asset_md
=
AssetMetadata
(
self
.
course_id
.
make_asset_key
(
'asset'
,
asset
[
0
]),
**
asset_dict
)
self
.
course_assets
.
append
(
asset_md
)
# Read in the XML schema definition and make a validator.
#xsd_path = path(__file__).abspath().dirname() / xsd_filename
xsd_path
=
path
(
__file__
)
.
realpath
()
.
parent
/
xsd_filename
with
open
(
xsd_path
,
'r'
)
as
f
:
schema_root
=
etree
.
XML
(
f
.
read
())
schema
=
etree
.
XMLSchema
(
schema_root
)
self
.
xmlparser
=
etree
.
XMLParser
(
schema
=
schema
)
def
test_export_single_asset_to_from_xml
(
self
):
"""
Export a single AssetMetadata to XML and verify the structure and fields.
"""
asset_md
=
self
.
course_assets
[
0
]
root
=
etree
.
Element
(
"assets"
)
asset
=
etree
.
SubElement
(
root
,
"asset"
)
asset_md
.
to_xml
(
asset
)
# If this line does *not* raise, the XML is valid.
etree
.
fromstring
(
etree
.
tostring
(
root
),
self
.
xmlparser
)
new_asset_key
=
self
.
course_id
.
make_asset_key
(
'tmp'
,
'tmp'
)
new_asset_md
=
AssetMetadata
(
new_asset_key
)
new_asset_md
.
from_xml
(
asset
)
# Compare asset_md to new_asset_md.
for
attr
in
AssetMetadata
.
ALL_ATTRS
:
orig_value
=
getattr
(
asset_md
,
attr
)
new_value
=
getattr
(
new_asset_md
,
attr
)
self
.
assertEqual
(
orig_value
,
new_value
)
def
test_export_all_assets_to_xml
(
self
):
"""
Export all AssetMetadatas to XML and verify the structure and fields.
"""
root
=
etree
.
Element
(
"assets"
)
AssetMetadata
.
add_all_assets_as_xml
(
root
,
self
.
course_assets
)
# If this line does *not* raise, the XML is valid.
etree
.
fromstring
(
etree
.
tostring
(
root
),
self
.
xmlparser
)
def
test_wrong_node_type_all
(
self
):
"""
Ensure full asset sections with the wrong tag are detected.
"""
root
=
etree
.
Element
(
"glassets"
)
with
self
.
assertRaises
(
ContractNotRespected
):
AssetMetadata
.
add_all_assets_as_xml
(
root
,
self
.
course_assets
)
def
test_wrong_node_type_single
(
self
):
"""
Ensure single asset blocks with the wrong tag are detected.
"""
asset_md
=
self
.
course_assets
[
0
]
root
=
etree
.
Element
(
"assets"
)
asset
=
etree
.
SubElement
(
root
,
"smashset"
)
with
self
.
assertRaises
(
ContractNotRespected
):
asset_md
.
to_xml
(
asset
)
common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py
View file @
29dcd88e
...
...
@@ -17,6 +17,32 @@ from xmodule.modulestore.tests.test_cross_modulestore_import_export import (
)
class
AssetStoreTestData
(
object
):
"""
Shared data for constructing test assets.
"""
now
=
datetime
.
now
(
pytz
.
utc
)
user_id
=
144
user_email
=
"me@example.com"
asset_fields
=
(
'filename'
,
'internal_name'
,
'basename'
,
'locked'
,
'edited_by'
,
'edited_by_email'
,
'edited_on'
,
'created_by'
,
'created_by_email'
,
'created_on'
,
'curr_version'
,
'prev_version'
)
all_asset_data
=
(
(
'pic1.jpg'
,
'EKMND332DDBK'
,
'pix/archive'
,
False
,
user_id
,
user_email
,
now
,
user_id
,
user_email
,
now
,
'14'
,
'13'
),
(
'shout.ogg'
,
'KFMDONSKF39K'
,
'sounds'
,
True
,
user_id
,
user_email
,
now
,
user_id
,
user_email
,
now
,
'1'
,
None
),
(
'code.tgz'
,
'ZZB2333YBDMW'
,
'exercises/14'
,
False
,
user_id
*
2
,
user_email
,
now
,
user_id
*
2
,
user_email
,
now
,
'AB'
,
'AA'
),
(
'dog.png'
,
'PUPY4242X'
,
'pictures/animals'
,
True
,
user_id
*
3
,
user_email
,
now
,
user_id
*
3
,
user_email
,
now
,
'5'
,
'4'
),
(
'not_here.txt'
,
'JJJCCC747'
,
'/dev/null'
,
False
,
user_id
*
4
,
user_email
,
now
,
user_id
*
4
,
user_email
,
now
,
'50'
,
'49'
),
(
'asset.txt'
,
'JJJCCC747858'
,
'/dev/null'
,
False
,
user_id
*
4
,
user_email
,
now
,
user_id
*
4
,
user_email
,
now
,
'50'
,
'49'
),
(
'roman_history.pdf'
,
'JASDUNSADK'
,
'texts/italy'
,
True
,
user_id
*
7
,
user_email
,
now
,
user_id
*
7
,
user_email
,
now
,
'1.1'
,
'1.01'
),
(
'weather_patterns.bmp'
,
'928SJXX2EB'
,
'science'
,
False
,
user_id
*
8
,
user_email
,
now
,
user_id
*
8
,
user_email
,
now
,
'52'
,
'51'
),
(
'demo.swf'
,
'DFDFGGGG14'
,
'demos/easy'
,
False
,
user_id
*
9
,
user_email
,
now
,
user_id
*
9
,
user_email
,
now
,
'5'
,
'4'
),
)
@ddt.ddt
class
TestMongoAssetMetadataStorage
(
unittest
.
TestCase
):
"""
...
...
@@ -33,7 +59,7 @@ class TestMongoAssetMetadataStorage(unittest.TestCase):
"""
if
type
(
mdata1
)
!=
type
(
mdata2
):
self
.
fail
(
self
.
_formatMessage
(
msg
,
u"{} is not same type as {}"
.
format
(
mdata1
,
mdata2
)))
for
attr
in
mdata1
.
A
LLOWED_ATTRS
:
for
attr
in
mdata1
.
A
TTRS_ALLOWED_TO_UPDATE
:
self
.
assertEqual
(
getattr
(
mdata1
,
attr
),
getattr
(
mdata2
,
attr
),
msg
)
def
_compare_datetimes
(
self
,
datetime1
,
datetime2
,
msg
=
None
):
...
...
@@ -68,27 +94,8 @@ class TestMongoAssetMetadataStorage(unittest.TestCase):
"""
Setup assets. Save in store if given
"""
asset_fields
=
(
'filename'
,
'internal_name'
,
'basename'
,
'locked'
,
'edited_by'
,
'edited_on'
,
'created_by'
,
'created_on'
,
'curr_version'
,
'prev_version'
)
now
=
datetime
.
now
(
pytz
.
utc
)
user_id
=
ModuleStoreEnum
.
UserID
.
test
all_asset_data
=
(
(
'pic1.jpg'
,
'EKMND332DDBK'
,
'pix/archive'
,
False
,
user_id
,
now
,
user_id
,
now
,
'14'
,
'13'
),
(
'shout.ogg'
,
'KFMDONSKF39K'
,
'sounds'
,
True
,
user_id
,
now
,
user_id
,
now
,
'1'
,
None
),
(
'code.tgz'
,
'ZZB2333YBDMW'
,
'exercises/14'
,
False
,
user_id
*
2
,
now
,
user_id
*
2
,
now
,
'AB'
,
'AA'
),
(
'dog.png'
,
'PUPY4242X'
,
'pictures/animals'
,
True
,
user_id
*
3
,
now
,
user_id
*
3
,
now
,
'5'
,
'4'
),
(
'not_here.txt'
,
'JJJCCC747'
,
'/dev/null'
,
False
,
user_id
*
4
,
now
,
user_id
*
4
,
now
,
'50'
,
'49'
),
(
'asset.txt'
,
'JJJCCC747858'
,
'/dev/null'
,
False
,
user_id
*
4
,
now
,
user_id
*
4
,
now
,
'50'
,
'49'
),
(
'roman_history.pdf'
,
'JASDUNSADK'
,
'texts/italy'
,
True
,
user_id
*
7
,
now
,
user_id
*
7
,
now
,
'1.1'
,
'1.01'
),
(
'weather_patterns.bmp'
,
'928SJXX2EB'
,
'science'
,
False
,
user_id
*
8
,
now
,
user_id
*
8
,
now
,
'52'
,
'51'
),
(
'demo.swf'
,
'DFDFGGGG14'
,
'demos/easy'
,
False
,
user_id
*
9
,
now
,
user_id
*
9
,
now
,
'5'
,
'4'
),
)
for
i
,
asset
in
enumerate
(
all_asset_data
):
asset_dict
=
dict
(
zip
(
asset_fields
[
1
:],
asset
[
1
:]))
for
i
,
asset
in
enumerate
(
AssetStoreTestData
.
all_asset_data
):
asset_dict
=
dict
(
zip
(
AssetStoreTestData
.
asset_fields
[
1
:],
asset
[
1
:]))
if
i
in
(
0
,
1
)
and
course1_key
:
asset_key
=
course1_key
.
make_asset_key
(
'asset'
,
asset
[
0
])
asset_md
=
AssetMetadata
(
asset_key
,
**
asset_dict
)
...
...
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