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
99e6d539
Commit
99e6d539
authored
Dec 22, 2016
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Preload definitions when collecting block structures
parent
82647ee8
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
288 additions
and
119 deletions
+288
-119
common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+10
-3
common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py
+70
-38
common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore_bulk_operations.py
+15
-0
common/lib/xmodule/xmodule/modulestore/tests/utils.py
+13
-12
lms/djangoapps/course_api/blocks/tests/test_api.py
+49
-2
lms/djangoapps/courseware/tests/test_views.py
+42
-0
lms/djangoapps/courseware/views/index.py
+1
-1
lms/djangoapps/grades/tasks.py
+37
-35
lms/djangoapps/grades/tests/test_tasks.py
+29
-16
lms/djangoapps/grades/tests/test_transformer.py
+12
-5
openedx/core/djangoapps/bookmarks/tests/test_tasks.py
+8
-5
openedx/core/lib/block_structure/factory.py
+1
-1
openedx/core/lib/block_structure/tests/helpers.py
+1
-1
No files found.
common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
View file @
99e6d539
...
@@ -432,9 +432,11 @@ class SplitBulkWriteMixin(BulkOperationsMixin):
...
@@ -432,9 +432,11 @@ class SplitBulkWriteMixin(BulkOperationsMixin):
if
len
(
ids
):
if
len
(
ids
):
# Query the db for the definitions.
# Query the db for the definitions.
defs_from_db
=
self
.
db_connection
.
get_definitions
(
list
(
ids
),
course_key
)
defs_from_db
=
list
(
self
.
db_connection
.
get_definitions
(
list
(
ids
),
course_key
))
defs_dict
=
{
d
.
get
(
'_id'
):
d
for
d
in
defs_from_db
}
# Add the retrieved definitions to the cache.
# Add the retrieved definitions to the cache.
bulk_write_record
.
definitions
.
update
({
d
.
get
(
'_id'
):
d
for
d
in
defs_from_db
})
bulk_write_record
.
definitions_in_db
.
update
(
defs_dict
.
iterkeys
())
bulk_write_record
.
definitions
.
update
(
defs_dict
)
definitions
.
extend
(
defs_from_db
)
definitions
.
extend
(
defs_from_db
)
return
definitions
return
definitions
...
@@ -772,11 +774,16 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -772,11 +774,16 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
Load the definitions into each block if lazy is in kwargs and is False;
Load the definitions into each block if lazy is in kwargs and is False;
otherwise, do not load the definitions - they'll be loaded later when needed.
otherwise, do not load the definitions - they'll be loaded later when needed.
"""
"""
lazy
=
kwargs
.
pop
(
'lazy'
,
True
)
should_cache_items
=
not
lazy
runtime
=
self
.
_get_cache
(
course_entry
.
structure
[
'_id'
])
runtime
=
self
.
_get_cache
(
course_entry
.
structure
[
'_id'
])
if
runtime
is
None
:
if
runtime
is
None
:
lazy
=
kwargs
.
pop
(
'lazy'
,
True
)
runtime
=
self
.
create_runtime
(
course_entry
,
lazy
)
runtime
=
self
.
create_runtime
(
course_entry
,
lazy
)
self
.
_add_cache
(
course_entry
.
structure
[
'_id'
],
runtime
)
self
.
_add_cache
(
course_entry
.
structure
[
'_id'
],
runtime
)
should_cache_items
=
True
if
should_cache_items
:
self
.
cache_items
(
runtime
,
block_keys
,
course_entry
.
course_key
,
depth
,
lazy
)
self
.
cache_items
(
runtime
,
block_keys
,
course_entry
.
course_key
,
depth
,
lazy
)
return
[
runtime
.
load_item
(
block_key
,
course_entry
,
**
kwargs
)
for
block_key
in
block_keys
]
return
[
runtime
.
load_item
(
block_key
,
course_entry
,
**
kwargs
)
for
block_key
in
block_keys
]
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py
View file @
99e6d539
...
@@ -13,7 +13,8 @@ from xmodule.modulestore.xml_exporter import export_course_to_xml
...
@@ -13,7 +13,8 @@ from xmodule.modulestore.xml_exporter import export_course_to_xml
from
xmodule.modulestore.tests.factories
import
check_mongo_calls
from
xmodule.modulestore.tests.factories
import
check_mongo_calls
from
xmodule.modulestore.tests.utils
import
(
from
xmodule.modulestore.tests.utils
import
(
MixedModulestoreBuilder
,
VersioningModulestoreBuilder
,
MixedModulestoreBuilder
,
VersioningModulestoreBuilder
,
MongoModulestoreBuilder
,
TEST_DATA_DIR
MongoModulestoreBuilder
,
TEST_DATA_DIR
,
MemoryCache
,
)
)
MIXED_OLD_MONGO_MODULESTORE_BUILDER
=
MixedModulestoreBuilder
([(
'draft'
,
MongoModulestoreBuilder
())])
MIXED_OLD_MONGO_MODULESTORE_BUILDER
=
MixedModulestoreBuilder
([(
'draft'
,
MongoModulestoreBuilder
())])
...
@@ -87,6 +88,46 @@ class CountMongoCallsCourseTraversal(TestCase):
...
@@ -87,6 +88,46 @@ class CountMongoCallsCourseTraversal(TestCase):
to the leaf nodes.
to the leaf nodes.
"""
"""
def
_traverse_blocks_in_course
(
self
,
course
,
access_all_block_fields
):
"""
Traverses all the blocks in the given course.
If access_all_block_fields is True, also reads all the
xblock fields in each block in the course.
"""
all_blocks
=
[]
stack
=
[
course
]
while
stack
:
curr_block
=
stack
.
pop
()
all_blocks
.
append
(
curr_block
)
if
curr_block
.
has_children
:
for
block
in
reversed
(
curr_block
.
get_children
()):
stack
.
append
(
block
)
if
access_all_block_fields
:
# Read the fields on each block in order to ensure each block and its definition is loaded.
for
xblock
in
all_blocks
:
for
__
,
field
in
xblock
.
fields
.
iteritems
():
if
field
.
is_set_on
(
xblock
):
__
=
field
.
read_from
(
xblock
)
def
_import_course
(
self
,
content_store
,
modulestore
):
"""
Imports a course for testing.
Returns the course key.
"""
course_key
=
modulestore
.
make_course_key
(
'a'
,
'course'
,
'course'
)
import_course_from_xml
(
modulestore
,
'test_user'
,
TEST_DATA_DIR
,
source_dirs
=
[
'manual-testing-complete'
],
static_content_store
=
content_store
,
target_id
=
course_key
,
create_if_not_present
=
True
,
raise_on_failure
=
True
,
)
return
course_key
# Suppose you want to traverse a course - maybe accessing the fields of each XBlock in the course,
# Suppose you want to traverse a course - maybe accessing the fields of each XBlock in the course,
# maybe not. What parameters should one use for get_course() in order to minimize the number of
# maybe not. What parameters should one use for get_course() in order to minimize the number of
# mongo calls? The tests below both ensure that code changes don't increase the number of mongo calls
# mongo calls? The tests below both ensure that code changes don't increase the number of mongo calls
...
@@ -109,52 +150,43 @@ class CountMongoCallsCourseTraversal(TestCase):
...
@@ -109,52 +150,43 @@ class CountMongoCallsCourseTraversal(TestCase):
# (if you'll eventually access all the fields and load all the definitions anyway).
# (if you'll eventually access all the fields and load all the definitions anyway).
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
None
,
False
,
True
,
4
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
None
,
False
,
True
,
4
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
None
,
True
,
True
,
38
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
None
,
True
,
True
,
38
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
0
,
False
,
True
,
131
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
0
,
False
,
True
,
38
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
0
,
True
,
True
,
38
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
0
,
True
,
True
,
38
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
None
,
False
,
False
,
4
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
None
,
False
,
False
,
4
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
None
,
True
,
False
,
4
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
None
,
True
,
False
,
4
),
# TODO: The call count below seems like a bug - should be 4?
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
0
,
False
,
False
,
4
),
# Seems to be related to using self.lazy in CachingDescriptorSystem.get_module_data().
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
0
,
False
,
False
,
131
),
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
0
,
True
,
False
,
4
)
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
0
,
True
,
False
,
4
)
)
)
@ddt.unpack
@ddt.unpack
def
test_number_mongo_calls
(
self
,
store
,
depth
,
lazy
,
access_all_block_fields
,
num_mongo_calls
):
def
test_number_mongo_calls
(
self
,
store_builder
,
depth
,
lazy
,
access_all_block_fields
,
num_mongo_calls
):
with
store
.
build
()
as
(
source_content
,
source_store
):
request_cache
=
MemoryCache
()
with
store_builder
.
build
(
request_cache
=
request_cache
)
as
(
content_store
,
modulestore
):
source_course_key
=
source_store
.
make_course_key
(
'a'
,
'course'
,
'course'
)
course_key
=
self
.
_import_course
(
content_store
,
modulestore
)
# First, import a course.
import_course_from_xml
(
source_store
,
'test_user'
,
TEST_DATA_DIR
,
source_dirs
=
[
'manual-testing-complete'
],
static_content_store
=
source_content
,
target_id
=
source_course_key
,
create_if_not_present
=
True
,
raise_on_failure
=
True
,
)
# Course traversal modeled after the traversal done here:
# Course traversal modeled after the traversal done here:
# lms/djangoapps/mobile_api/video_outlines/serializers.py:BlockOutline
# lms/djangoapps/mobile_api/video_outlines/serializers.py:BlockOutline
# Starting at the root course block, do a breadth-first traversal using
# Starting at the root course block, do a breadth-first traversal using
# get_children() to retrieve each block's children.
# get_children() to retrieve each block's children.
with
check_mongo_calls
(
num_mongo_calls
):
with
check_mongo_calls
(
num_mongo_calls
):
with
source_store
.
bulk_operations
(
source_course_key
):
with
modulestore
.
bulk_operations
(
course_key
):
start_block
=
source_store
.
get_course
(
source_course_key
,
depth
=
depth
,
lazy
=
lazy
)
start_block
=
modulestore
.
get_course
(
course_key
,
depth
=
depth
,
lazy
=
lazy
)
all_blocks
=
[]
self
.
_traverse_blocks_in_course
(
start_block
,
access_all_block_fields
)
stack
=
[
start_block
]
while
stack
:
@ddt.data
(
curr_block
=
stack
.
pop
()
(
MIXED_OLD_MONGO_MODULESTORE_BUILDER
,
176
),
all_blocks
.
append
(
curr_block
)
(
MIXED_SPLIT_MODULESTORE_BUILDER
,
5
),
if
curr_block
.
has_children
:
)
for
block
in
reversed
(
curr_block
.
get_children
()):
@ddt.unpack
stack
.
append
(
block
)
def
test_lazy_when_course_previously_cached
(
self
,
store_builder
,
num_mongo_calls
):
request_cache
=
MemoryCache
()
if
access_all_block_fields
:
with
store_builder
.
build
(
request_cache
=
request_cache
)
as
(
content_store
,
modulestore
):
# Read the fields on each block in order to ensure each block and its definition is loaded.
course_key
=
self
.
_import_course
(
content_store
,
modulestore
)
for
xblock
in
all_blocks
:
for
__
,
field
in
xblock
.
fields
.
iteritems
():
with
check_mongo_calls
(
num_mongo_calls
):
if
field
.
is_set_on
(
xblock
):
with
modulestore
.
bulk_operations
(
course_key
):
__
=
field
.
read_from
(
xblock
)
# assume the course was retrieved earlier
course
=
modulestore
.
get_course
(
course_key
,
depth
=
0
,
lazy
=
True
)
# and then subsequently retrieved with the lazy and depth=None values
course
=
modulestore
.
get_item
(
course
.
location
,
depth
=
None
,
lazy
=
False
)
self
.
_traverse_blocks_in_course
(
course
,
access_all_block_fields
=
True
)
common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore_bulk_operations.py
View file @
99e6d539
"""
Tests for bulk operations in Split Modulestore.
"""
# pylint: disable=protected-access
import
copy
import
copy
import
ddt
import
ddt
import
unittest
import
unittest
...
@@ -444,6 +448,17 @@ class TestBulkWriteMixinFindMethods(TestBulkWriteMixin):
...
@@ -444,6 +448,17 @@ class TestBulkWriteMixinFindMethods(TestBulkWriteMixin):
else
:
else
:
self
.
assertNotIn
(
db_definition
(
_id
),
results
)
self
.
assertNotIn
(
db_definition
(
_id
),
results
)
def
test_get_definitions_doesnt_update_db
(
self
):
test_ids
=
[
1
,
2
]
db_definition
=
lambda
_id
:
{
'db'
:
'definition'
,
'_id'
:
_id
}
db_definitions
=
[
db_definition
(
_id
)
for
_id
in
test_ids
]
self
.
conn
.
get_definitions
.
return_value
=
db_definitions
self
.
bulk
.
_begin_bulk_operation
(
self
.
course_key
)
self
.
bulk
.
get_definitions
(
self
.
course_key
,
test_ids
)
self
.
bulk
.
_end_bulk_operation
(
self
.
course_key
)
self
.
assertFalse
(
self
.
conn
.
insert_definition
.
called
)
def
test_no_bulk_find_structures_derived_from
(
self
):
def
test_no_bulk_find_structures_derived_from
(
self
):
ids
=
[
Mock
(
name
=
'id'
)]
ids
=
[
Mock
(
name
=
'id'
)]
self
.
conn
.
find_structures_derived_from
.
return_value
=
[
MagicMock
(
name
=
'result'
)]
self
.
conn
.
find_structures_derived_from
.
return_value
=
[
MagicMock
(
name
=
'result'
)]
...
...
common/lib/xmodule/xmodule/modulestore/tests/utils.py
View file @
99e6d539
...
@@ -194,7 +194,7 @@ class MemoryCache(object):
...
@@ -194,7 +194,7 @@ class MemoryCache(object):
the modulestore, and stores the data in a dictionary in memory.
the modulestore, and stores the data in a dictionary in memory.
"""
"""
def
__init__
(
self
):
def
__init__
(
self
):
self
.
_
data
=
{}
self
.
data
=
{}
def
get
(
self
,
key
,
default
=
None
):
def
get
(
self
,
key
,
default
=
None
):
"""
"""
...
@@ -204,7 +204,7 @@ class MemoryCache(object):
...
@@ -204,7 +204,7 @@ class MemoryCache(object):
key: The key to update.
key: The key to update.
default: The value to return if the key hasn't been set previously.
default: The value to return if the key hasn't been set previously.
"""
"""
return
self
.
_
data
.
get
(
key
,
default
)
return
self
.
data
.
get
(
key
,
default
)
def
set
(
self
,
key
,
value
):
def
set
(
self
,
key
,
value
):
"""
"""
...
@@ -214,7 +214,7 @@ class MemoryCache(object):
...
@@ -214,7 +214,7 @@ class MemoryCache(object):
key: The key to update.
key: The key to update.
value: The value change the key to.
value: The value change the key to.
"""
"""
self
.
_
data
[
key
]
=
value
self
.
data
[
key
]
=
value
class
MongoContentstoreBuilder
(
object
):
class
MongoContentstoreBuilder
(
object
):
...
@@ -255,19 +255,19 @@ class StoreBuilderBase(object):
...
@@ -255,19 +255,19 @@ class StoreBuilderBase(object):
"""
"""
contentstore
=
kwargs
.
pop
(
'contentstore'
,
None
)
contentstore
=
kwargs
.
pop
(
'contentstore'
,
None
)
if
not
contentstore
:
if
not
contentstore
:
with
self
.
build_without_contentstore
()
as
(
contentstore
,
modulestore
):
with
self
.
build_without_contentstore
(
**
kwargs
)
as
(
contentstore
,
modulestore
):
yield
contentstore
,
modulestore
yield
contentstore
,
modulestore
else
:
else
:
with
self
.
build_with_contentstore
(
contentstore
)
as
modulestore
:
with
self
.
build_with_contentstore
(
contentstore
,
**
kwargs
)
as
modulestore
:
yield
modulestore
yield
modulestore
@contextmanager
@contextmanager
def
build_without_contentstore
(
self
):
def
build_without_contentstore
(
self
,
**
kwargs
):
"""
"""
Build both the contentstore and the modulestore.
Build both the contentstore and the modulestore.
"""
"""
with
MongoContentstoreBuilder
()
.
build
()
as
contentstore
:
with
MongoContentstoreBuilder
()
.
build
()
as
contentstore
:
with
self
.
build_with_contentstore
(
contentstore
)
as
modulestore
:
with
self
.
build_with_contentstore
(
contentstore
,
**
kwargs
)
as
modulestore
:
yield
contentstore
,
modulestore
yield
contentstore
,
modulestore
...
@@ -276,7 +276,7 @@ class MongoModulestoreBuilder(StoreBuilderBase):
...
@@ -276,7 +276,7 @@ class MongoModulestoreBuilder(StoreBuilderBase):
A builder class for a DraftModuleStore.
A builder class for a DraftModuleStore.
"""
"""
@contextmanager
@contextmanager
def
build_with_contentstore
(
self
,
contentstore
):
def
build_with_contentstore
(
self
,
contentstore
,
**
kwargs
):
"""
"""
A contextmanager that returns an isolated mongo modulestore, and then deletes
A contextmanager that returns an isolated mongo modulestore, and then deletes
all of its data at the end of the context.
all of its data at the end of the context.
...
@@ -324,7 +324,7 @@ class VersioningModulestoreBuilder(StoreBuilderBase):
...
@@ -324,7 +324,7 @@ class VersioningModulestoreBuilder(StoreBuilderBase):
A builder class for a VersioningModuleStore.
A builder class for a VersioningModuleStore.
"""
"""
@contextmanager
@contextmanager
def
build_with_contentstore
(
self
,
contentstore
):
def
build_with_contentstore
(
self
,
contentstore
,
**
kwargs
):
"""
"""
A contextmanager that returns an isolated versioning modulestore, and then deletes
A contextmanager that returns an isolated versioning modulestore, and then deletes
all of its data at the end of the context.
all of its data at the end of the context.
...
@@ -347,6 +347,7 @@ class VersioningModulestoreBuilder(StoreBuilderBase):
...
@@ -347,6 +347,7 @@ class VersioningModulestoreBuilder(StoreBuilderBase):
fs_root
,
fs_root
,
render_template
=
repr
,
render_template
=
repr
,
xblock_mixins
=
XBLOCK_MIXINS
,
xblock_mixins
=
XBLOCK_MIXINS
,
**
kwargs
)
)
modulestore
.
ensure_indexes
()
modulestore
.
ensure_indexes
()
...
@@ -369,7 +370,7 @@ class XmlModulestoreBuilder(StoreBuilderBase):
...
@@ -369,7 +370,7 @@ class XmlModulestoreBuilder(StoreBuilderBase):
"""
"""
# pylint: disable=unused-argument
# pylint: disable=unused-argument
@contextmanager
@contextmanager
def
build_with_contentstore
(
self
,
contentstore
=
None
,
course_ids
=
None
):
def
build_with_contentstore
(
self
,
contentstore
=
None
,
course_ids
=
None
,
**
kwargs
):
"""
"""
A contextmanager that returns an isolated xml modulestore
A contextmanager that returns an isolated xml modulestore
...
@@ -403,7 +404,7 @@ class MixedModulestoreBuilder(StoreBuilderBase):
...
@@ -403,7 +404,7 @@ class MixedModulestoreBuilder(StoreBuilderBase):
self
.
mixed_modulestore
=
None
self
.
mixed_modulestore
=
None
@contextmanager
@contextmanager
def
build_with_contentstore
(
self
,
contentstore
):
def
build_with_contentstore
(
self
,
contentstore
,
**
kwargs
):
"""
"""
A contextmanager that returns a mixed modulestore built on top of modulestores
A contextmanager that returns a mixed modulestore built on top of modulestores
generated by other builder classes.
generated by other builder classes.
...
@@ -414,7 +415,7 @@ class MixedModulestoreBuilder(StoreBuilderBase):
...
@@ -414,7 +415,7 @@ class MixedModulestoreBuilder(StoreBuilderBase):
"""
"""
names
,
generators
=
zip
(
*
self
.
store_builders
)
names
,
generators
=
zip
(
*
self
.
store_builders
)
with
nested
(
*
(
gen
.
build_with_contentstore
(
contentstore
)
for
gen
in
generators
))
as
modulestores
:
with
nested
(
*
(
gen
.
build_with_contentstore
(
contentstore
,
**
kwargs
)
for
gen
in
generators
))
as
modulestores
:
# Make the modulestore creation function just return the already-created modulestores
# Make the modulestore creation function just return the already-created modulestores
store_iterator
=
iter
(
modulestores
)
store_iterator
=
iter
(
modulestores
)
next_modulestore
=
lambda
*
args
,
**
kwargs
:
store_iterator
.
next
()
next_modulestore
=
lambda
*
args
,
**
kwargs
:
store_iterator
.
next
()
...
...
lms/djangoapps/course_api/blocks/tests/test_api.py
View file @
99e6d539
...
@@ -2,12 +2,14 @@
...
@@ -2,12 +2,14 @@
Tests for Blocks api.py
Tests for Blocks api.py
"""
"""
import
ddt
from
django.test.client
import
RequestFactory
from
django.test.client
import
RequestFactory
from
openedx.core.djangoapps.content.block_structure.api
import
clear_course_from_cache
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
SampleCourseFactory
from
xmodule.modulestore.tests.factories
import
SampleCourseFactory
,
check_mongo_calls
from
..api
import
get_blocks
from
..api
import
get_blocks
...
@@ -19,7 +21,8 @@ class TestGetBlocks(SharedModuleStoreTestCase):
...
@@ -19,7 +21,8 @@ class TestGetBlocks(SharedModuleStoreTestCase):
@classmethod
@classmethod
def
setUpClass
(
cls
):
def
setUpClass
(
cls
):
super
(
TestGetBlocks
,
cls
)
.
setUpClass
()
super
(
TestGetBlocks
,
cls
)
.
setUpClass
()
cls
.
course
=
SampleCourseFactory
.
create
()
with
cls
.
store
.
default_store
(
ModuleStoreEnum
.
Type
.
split
):
cls
.
course
=
SampleCourseFactory
.
create
()
# hide the html block
# hide the html block
cls
.
html_block
=
cls
.
store
.
get_item
(
cls
.
course
.
id
.
make_usage_key
(
'html'
,
'html_x1a_1'
))
cls
.
html_block
=
cls
.
store
.
get_item
(
cls
.
course
.
id
.
make_usage_key
(
'html'
,
'html_x1a_1'
))
...
@@ -95,3 +98,47 @@ class TestGetBlocks(SharedModuleStoreTestCase):
...
@@ -95,3 +98,47 @@ class TestGetBlocks(SharedModuleStoreTestCase):
self
.
assertEquals
(
len
(
blocks
[
'blocks'
]),
3
)
self
.
assertEquals
(
len
(
blocks
[
'blocks'
]),
3
)
for
block
in
blocks
[
'blocks'
]
.
itervalues
():
for
block
in
blocks
[
'blocks'
]
.
itervalues
():
self
.
assertEqual
(
block
[
'type'
],
'problem'
)
self
.
assertEqual
(
block
[
'type'
],
'problem'
)
@ddt.ddt
class
TestGetBlocksQueryCounts
(
SharedModuleStoreTestCase
):
"""
Tests query counts for the get_blocks function.
"""
def
setUp
(
self
):
super
(
TestGetBlocksQueryCounts
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
request
=
RequestFactory
()
.
get
(
"/dummy"
)
self
.
request
.
user
=
self
.
user
def
_create_course
(
self
,
store_type
):
"""
Creates the sample course in the given store type.
"""
with
self
.
store
.
default_store
(
store_type
):
return
SampleCourseFactory
.
create
()
def
_get_blocks
(
self
,
course
,
expected_mongo_queries
):
"""
Verifies the number of expected queries when calling
get_blocks on the given course.
"""
with
check_mongo_calls
(
expected_mongo_queries
):
with
self
.
assertNumQueries
(
2
):
get_blocks
(
self
.
request
,
course
.
location
,
self
.
user
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
def
test_query_counts_cached
(
self
,
store_type
):
course
=
self
.
_create_course
(
store_type
)
self
.
_get_blocks
(
course
,
expected_mongo_queries
=
0
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
5
),
(
ModuleStoreEnum
.
Type
.
split
,
3
),
)
@ddt.unpack
def
test_query_counts_uncached
(
self
,
store_type
,
expected_mongo_queries
):
course
=
self
.
_create_course
(
store_type
)
clear_course_from_cache
(
course
.
id
)
self
.
_get_blocks
(
course
,
expected_mongo_queries
)
lms/djangoapps/courseware/tests/test_views.py
View file @
99e6d539
...
@@ -191,6 +191,48 @@ class TestJumpTo(ModuleStoreTestCase):
...
@@ -191,6 +191,48 @@ class TestJumpTo(ModuleStoreTestCase):
@attr
(
shard
=
2
)
@attr
(
shard
=
2
)
@ddt.ddt
@ddt.ddt
class
IndexQueryTestCase
(
ModuleStoreTestCase
):
"""
Tests for query count.
"""
CREATE_USER
=
False
NUM_PROBLEMS
=
20
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
8
),
(
ModuleStoreEnum
.
Type
.
split
,
4
),
)
@ddt.unpack
def
test_index_query_counts
(
self
,
store_type
,
expected_query_count
):
with
self
.
store
.
default_store
(
store_type
):
course
=
CourseFactory
.
create
()
with
self
.
store
.
bulk_operations
(
course
.
id
):
chapter
=
ItemFactory
.
create
(
category
=
'chapter'
,
parent_location
=
course
.
location
)
section
=
ItemFactory
.
create
(
category
=
'sequential'
,
parent_location
=
chapter
.
location
)
vertical
=
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
section
.
location
)
for
_
in
range
(
self
.
NUM_PROBLEMS
):
ItemFactory
.
create
(
category
=
'problem'
,
parent_location
=
vertical
.
location
)
password
=
'test'
self
.
user
=
UserFactory
(
password
=
password
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
password
)
CourseEnrollment
.
enroll
(
self
.
user
,
course
.
id
)
with
check_mongo_calls
(
expected_query_count
):
url
=
reverse
(
'courseware_section'
,
kwargs
=
{
'course_id'
:
unicode
(
course
.
id
),
'chapter'
:
unicode
(
chapter
.
location
.
name
),
'section'
:
unicode
(
section
.
location
.
name
),
}
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
@attr
(
shard
=
2
)
@ddt.ddt
class
ViewsTestCase
(
ModuleStoreTestCase
):
class
ViewsTestCase
(
ModuleStoreTestCase
):
"""
"""
Tests for views.py methods.
Tests for views.py methods.
...
...
lms/djangoapps/courseware/views/index.py
View file @
99e6d539
...
@@ -348,7 +348,7 @@ class CoursewareIndex(View):
...
@@ -348,7 +348,7 @@ class CoursewareIndex(View):
sets up the runtime, which binds the request user to the section.
sets up the runtime, which binds the request user to the section.
"""
"""
# Pre-fetch all descendant data
# Pre-fetch all descendant data
self
.
section
=
modulestore
()
.
get_item
(
self
.
section
.
location
,
depth
=
None
)
self
.
section
=
modulestore
()
.
get_item
(
self
.
section
.
location
,
depth
=
None
,
lazy
=
False
)
self
.
field_data_cache
.
add_descriptor_descendents
(
self
.
section
,
depth
=
None
)
self
.
field_data_cache
.
add_descriptor_descendents
(
self
.
section
,
depth
=
None
)
# Bind section to user
# Bind section to user
...
...
lms/djangoapps/grades/tasks.py
View file @
99e6d539
...
@@ -158,43 +158,45 @@ def _update_subsection_grades(
...
@@ -158,43 +158,45 @@ def _update_subsection_grades(
that those subsection grades were updated.
that those subsection grades were updated.
"""
"""
student
=
User
.
objects
.
get
(
id
=
user_id
)
student
=
User
.
objects
.
get
(
id
=
user_id
)
course_structure
=
get_course_blocks
(
student
,
modulestore
()
.
make_course_usage_key
(
course_key
))
store
=
modulestore
()
subsections_to_update
=
course_structure
.
get_transformer_block_field
(
with
store
.
bulk_operations
(
course_key
):
scored_block_usage_key
,
course_structure
=
get_course_blocks
(
student
,
store
.
make_course_usage_key
(
course_key
))
GradesTransformer
,
subsections_to_update
=
course_structure
.
get_transformer_block_field
(
'subsections'
,
scored_block_usage_key
,
set
(),
GradesTransformer
,
)
'subsections'
,
set
(),
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
0
)
subsection_grade_factory
=
SubsectionGradeFactory
(
student
,
course
,
course_structure
)
try
:
for
subsection_usage_key
in
subsections_to_update
:
if
subsection_usage_key
in
course_structure
:
subsection_grade
=
subsection_grade_factory
.
update
(
course_structure
[
subsection_usage_key
],
only_if_higher
,
)
SUBSECTION_SCORE_CHANGED
.
send
(
sender
=
recalculate_subsection_grade
,
course
=
course
,
course_structure
=
course_structure
,
user
=
student
,
subsection_grade
=
subsection_grade
,
)
except
DatabaseError
as
exc
:
raise
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
expected_modified_time
,
score_deleted
,
exc
,
)
)
course
=
store
.
get_course
(
course_key
,
depth
=
0
)
subsection_grade_factory
=
SubsectionGradeFactory
(
student
,
course
,
course_structure
)
try
:
for
subsection_usage_key
in
subsections_to_update
:
if
subsection_usage_key
in
course_structure
:
subsection_grade
=
subsection_grade_factory
.
update
(
course_structure
[
subsection_usage_key
],
only_if_higher
,
)
SUBSECTION_SCORE_CHANGED
.
send
(
sender
=
recalculate_subsection_grade
,
course
=
course
,
course_structure
=
course_structure
,
user
=
student
,
subsection_grade
=
subsection_grade
,
)
except
DatabaseError
as
exc
:
raise
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
expected_modified_time
,
score_deleted
,
exc
,
)
def
_retry_recalculate_subsection_grade
(
def
_retry_recalculate_subsection_grade
(
user_id
,
user_id
,
...
...
lms/djangoapps/grades/tests/test_tasks.py
View file @
99e6d539
...
@@ -121,16 +121,17 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -121,16 +121,17 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
assertTrue
(
mock_subsection_signal
.
called
)
self
.
assertTrue
(
mock_subsection_signal
.
called
)
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
23
),
(
ModuleStoreEnum
.
Type
.
split
,
0
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
22
),
)
)
@ddt.unpack
@ddt.unpack
def
test_subsection_grade_updated
(
self
,
default_store
,
added_querie
s
):
def
test_subsection_grade_updated
(
self
,
default_store
,
num_mongo_calls
,
num_sql_call
s
):
with
self
.
store
.
default_store
(
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
22
+
added_queries
):
with
check_mongo_calls
(
num_mongo_calls
):
self
.
_apply_recalculate_subsection_grade
()
with
self
.
assertNumQueries
(
num_sql_calls
):
self
.
_apply_recalculate_subsection_grade
()
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
def
test_other_inaccessible_subsection
(
self
,
mock_subsection_signal
):
def
test_other_inaccessible_subsection
(
self
,
mock_subsection_signal
):
...
@@ -166,27 +167,38 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -166,27 +167,38 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
# TODO (TNL-6225) Fix the number of SQL queries so they
# don't grow linearly with the number of sequentials.
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
46
),
(
ModuleStoreEnum
.
Type
.
split
,
0
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
45
),
)
)
@ddt.unpack
@ddt.unpack
def
test_query_count_does_not_change_with_more_
problems
(
self
,
default_store
,
added_querie
s
):
def
test_query_count_does_not_change_with_more_
content
(
self
,
default_store
,
num_mongo_calls
,
num_sql_call
s
):
with
self
.
store
.
default_store
(
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem2'
)
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem3'
)
num_problems
=
10
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
22
+
added_queries
):
for
_
in
range
(
num_problems
):
self
.
_apply_recalculate_subsection_grade
()
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
)
num_sequentials
=
10
for
_
in
range
(
num_sequentials
):
ItemFactory
.
create
(
parent
=
self
.
chapter
,
category
=
'sequential'
)
with
check_mongo_calls
(
num_mongo_calls
):
with
self
.
assertNumQueries
(
num_sql_calls
):
self
.
_apply_recalculate_subsection_grade
()
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
def
test_subsection_grades_not_enabled_on_course
(
self
,
default_store
):
def
test_subsection_grades_not_enabled_on_course
(
self
,
default_store
):
with
self
.
store
.
default_store
(
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
(
enable_subsection_grades
=
False
)
self
.
set_up_course
(
enable_subsection_grades
=
False
)
self
.
assertFalse
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
self
.
assertFalse
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
0
):
with
check_mongo_calls
(
0
):
self
.
_apply_recalculate_subsection_grade
()
with
self
.
assertNumQueries
(
0
):
self
.
_apply_recalculate_subsection_grade
()
@skip
(
"Pending completion of TNL-5089"
)
@skip
(
"Pending completion of TNL-5089"
)
@ddt.data
(
@ddt.data
(
...
@@ -200,8 +212,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -200,8 +212,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
PersistentGradesEnabledFlag
.
objects
.
create
(
enabled
=
feature_flag
)
PersistentGradesEnabledFlag
.
objects
.
create
(
enabled
=
feature_flag
)
with
self
.
store
.
default_store
(
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
set_up_course
()
with
check_mongo_calls
(
0
)
and
self
.
assertNumQueries
(
3
if
feature_flag
else
2
):
with
check_mongo_calls
(
0
):
self
.
_apply_recalculate_subsection_grade
()
with
self
.
assertNumQueries
(
3
if
feature_flag
else
2
):
self
.
_apply_recalculate_subsection_grade
()
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry'
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
...
...
lms/djangoapps/grades/tests/test_transformer.py
View file @
99e6d539
...
@@ -17,7 +17,7 @@ from xmodule.modulestore.tests.factories import check_mongo_calls
...
@@ -17,7 +17,7 @@ from xmodule.modulestore.tests.factories import check_mongo_calls
from
lms.djangoapps.course_blocks.api
import
get_course_blocks
from
lms.djangoapps.course_blocks.api
import
get_course_blocks
from
lms.djangoapps.course_blocks.transformers.tests.helpers
import
CourseStructureTestCase
from
lms.djangoapps.course_blocks.transformers.tests.helpers
import
CourseStructureTestCase
from
openedx.core.djangoapps.content.block_structure.api
import
get
_cache
from
openedx.core.djangoapps.content.block_structure.api
import
clear_course_from
_cache
from
..transformer
import
GradesTransformer
from
..transformer
import
GradesTransformer
...
@@ -390,6 +390,7 @@ class GradesTransformerTestCase(CourseStructureTestCase):
...
@@ -390,6 +390,7 @@ class GradesTransformerTestCase(CourseStructureTestCase):
)
)
@ddt.ddt
class
MultiProblemModulestoreAccessTestCase
(
CourseStructureTestCase
,
SharedModuleStoreTestCase
):
class
MultiProblemModulestoreAccessTestCase
(
CourseStructureTestCase
,
SharedModuleStoreTestCase
):
"""
"""
Test mongo usage in GradesTransformer.
Test mongo usage in GradesTransformer.
...
@@ -403,7 +404,12 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul
...
@@ -403,7 +404,12 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul
self
.
student
=
UserFactory
.
create
(
is_staff
=
False
,
username
=
u'test_student'
,
password
=
password
)
self
.
student
=
UserFactory
.
create
(
is_staff
=
False
,
username
=
u'test_student'
,
password
=
password
)
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
password
)
self
.
client
.
login
(
username
=
self
.
student
.
username
,
password
=
password
)
def
test_modulestore_performance
(
self
):
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
split
,
3
),
(
ModuleStoreEnum
.
Type
.
mongo
,
2
),
)
@ddt.unpack
def
test_modulestore_performance
(
self
,
store_type
,
expected_mongo_queries
):
"""
"""
Test that a constant number of mongo calls are made regardless of how
Test that a constant number of mongo calls are made regardless of how
many grade-related blocks are in the course.
many grade-related blocks are in the course.
...
@@ -437,7 +443,8 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul
...
@@ -437,7 +443,8 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul
</problem>'''
.
format
(
number
=
problem_number
),
</problem>'''
.
format
(
number
=
problem_number
),
}
}
)
)
blocks
=
self
.
build_course
(
course
)
with
self
.
store
.
default_store
(
store_type
):
get_cache
()
.
clear
()
blocks
=
self
.
build_course
(
course
)
with
check_mongo_calls
(
2
):
clear_course_from_cache
(
blocks
[
u'course'
]
.
id
)
with
check_mongo_calls
(
expected_mongo_queries
):
get_course_blocks
(
self
.
student
,
blocks
[
u'course'
]
.
location
,
self
.
transformers
)
get_course_blocks
(
self
.
student
,
blocks
[
u'course'
]
.
location
,
self
.
transformers
)
openedx/core/djangoapps/bookmarks/tests/test_tasks.py
View file @
99e6d539
...
@@ -103,11 +103,11 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
...
@@ -103,11 +103,11 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
}
}
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
2
,
2
,
3
),
(
ModuleStoreEnum
.
Type
.
mongo
,
2
,
2
,
4
),
(
ModuleStoreEnum
.
Type
.
mongo
,
4
,
2
,
3
),
(
ModuleStoreEnum
.
Type
.
mongo
,
4
,
2
,
4
),
(
ModuleStoreEnum
.
Type
.
mongo
,
2
,
3
,
4
),
(
ModuleStoreEnum
.
Type
.
mongo
,
2
,
3
,
5
),
(
ModuleStoreEnum
.
Type
.
mongo
,
4
,
3
,
4
),
(
ModuleStoreEnum
.
Type
.
mongo
,
4
,
3
,
5
),
(
ModuleStoreEnum
.
Type
.
mongo
,
2
,
4
,
5
),
(
ModuleStoreEnum
.
Type
.
mongo
,
2
,
4
,
6
),
# (ModuleStoreEnum.Type.mongo, 4, 4, 6), Too slow.
# (ModuleStoreEnum.Type.mongo, 4, 4, 6), Too slow.
(
ModuleStoreEnum
.
Type
.
split
,
2
,
2
,
3
),
(
ModuleStoreEnum
.
Type
.
split
,
2
,
2
,
3
),
(
ModuleStoreEnum
.
Type
.
split
,
4
,
2
,
3
),
(
ModuleStoreEnum
.
Type
.
split
,
4
,
2
,
3
),
...
@@ -119,6 +119,9 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
...
@@ -119,6 +119,9 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
course
=
self
.
create_course_with_blocks
(
children_per_block
,
depth
,
store_type
)
course
=
self
.
create_course_with_blocks
(
children_per_block
,
depth
,
store_type
)
# clear cache to get consistent query counts
self
.
clear_caches
()
with
check_mongo_calls
(
expected_mongo_calls
):
with
check_mongo_calls
(
expected_mongo_calls
):
blocks_data
=
_calculate_course_xblocks_data
(
course
.
id
)
blocks_data
=
_calculate_course_xblocks_data
(
course
.
id
)
self
.
assertGreater
(
len
(
blocks_data
),
children_per_block
**
depth
)
self
.
assertGreater
(
len
(
blocks_data
),
children_per_block
**
depth
)
...
...
openedx/core/lib/block_structure/factory.py
View file @
99e6d539
...
@@ -53,7 +53,7 @@ class BlockStructureFactory(object):
...
@@ -53,7 +53,7 @@ class BlockStructureFactory(object):
block_structure
.
_add_relation
(
xblock
.
location
,
child
.
location
)
# pylint: disable=protected-access
block_structure
.
_add_relation
(
xblock
.
location
,
child
.
location
)
# pylint: disable=protected-access
build_block_structure
(
child
)
build_block_structure
(
child
)
root_xblock
=
modulestore
.
get_item
(
root_block_usage_key
,
depth
=
None
)
root_xblock
=
modulestore
.
get_item
(
root_block_usage_key
,
depth
=
None
,
lazy
=
False
)
build_block_structure
(
root_xblock
)
build_block_structure
(
root_xblock
)
return
block_structure
return
block_structure
...
...
openedx/core/lib/block_structure/tests/helpers.py
View file @
99e6d539
...
@@ -55,7 +55,7 @@ class MockModulestore(object):
...
@@ -55,7 +55,7 @@ class MockModulestore(object):
"""
"""
self
.
blocks
=
blocks
self
.
blocks
=
blocks
def
get_item
(
self
,
block_key
,
depth
=
None
):
# pylint: disable=unused-argument
def
get_item
(
self
,
block_key
,
depth
=
None
,
lazy
=
False
):
# pylint: disable=unused-argument
"""
"""
Returns the mock XBlock (MockXBlock) associated with the
Returns the mock XBlock (MockXBlock) associated with the
given block_key.
given block_key.
...
...
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