Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions cms/djangoapps/contentstore/tests/test_libraries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
"""
Content library unit tests that require the CMS runtime.
"""
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json
from contentstore.utils import reverse_usage_url
from contentstore.views.tests.test_library import LIBRARY_REST_URL
from fs.memoryfs import MemoryFS
from xmodule.library_content_module import LibraryVersionReference
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.tests import get_test_system
from mock import Mock
from opaque_keys.edx.locator import CourseKey, LibraryLocator
import ddt


@ddt.ddt
class TestLibraries(ModuleStoreTestCase):
"""
High-level tests for libraries
"""
def setUp(self):
user_password = super(TestLibraries, self).setUp()

self.client = AjaxEnabledTestClient()
self.client.login(username=self.user.username, password=user_password)

self.lib_key = self._create_library()
self.library = modulestore().get_library(self.lib_key)

def _create_module_system(self, course):
"""
Create an xmodule system so we can use bind_for_student
"""
def get_module(descriptor):
"""Mocks module_system get_module function"""
module_system = get_test_system()
module_system.get_module = get_module
descriptor.bind_for_student(module_system, descriptor._field_data) # pylint: disable=protected-access
return descriptor

module_system = get_test_system()
module_system.get_module = get_module
module_system.descriptor_system = course.runtime
course.runtime.export_fs = MemoryFS()
return module_system

def _create_library(self, org="org", library="lib", display_name="Test Library"):
"""
Helper method used to create a library. Uses the REST API.
"""
response = self.client.ajax_post(LIBRARY_REST_URL, {
'org': org,
'library': library,
'display_name': display_name,
})
self.assertEqual(response.status_code, 200)
lib_info = parse_json(response)
lib_key = CourseKey.from_string(lib_info['library_key'])
self.assertIsInstance(lib_key, LibraryLocator)
return lib_key

def _add_library_content_block(self, course, library_key, other_settings=None):
"""
Helper method to add a LibraryContent block to a course.
The block will be configured to select content from the library
specified by library_key.
other_settings can be a dict of Scope.settings fields to set on the block.
"""
metadata = {'source_libraries': [LibraryVersionReference(library_key)]}
if other_settings:
metadata.update(other_settings)
return ItemFactory.create(
category='library_content',
parent_location=course.location,
user_id=self.user.id,
metadata=metadata,
publish_item=False,
)

def _refresh_children(self, lib_content_block):
"""
Helper method: Uses the REST API to call the 'refresh_children' handler
of a LibraryContent block
"""
if 'user' not in lib_content_block.runtime._services: # pylint: disable=protected-access
lib_content_block.runtime._services['user'] = Mock(user_id=self.user.id) # pylint: disable=protected-access
handler_url = reverse_usage_url('component_handler', lib_content_block.location, kwargs={'handler': 'refresh_children'})
response = self.client.ajax_post(handler_url)
self.assertEqual(response.status_code, 200)
return modulestore().get_item(lib_content_block.location)

@ddt.data(
(2, 1, 1),
(2, 2, 2),
(2, 20, 2),
)
@ddt.unpack
def test_max_items(self, num_to_create, num_to_select, num_expected):
"""
Test the 'max_count' property of LibraryContent blocks.
"""
for _ in range(0, num_to_create):
ItemFactory.create(category="html", parent_location=self.library.location, user_id=self.user.id, publish_item=False)

with modulestore().default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()

lc_block = self._add_library_content_block(course, self.lib_key, {'max_count': num_to_select})
self.assertEqual(len(lc_block.children), 0)
lc_block = self._refresh_children(lc_block)

# Now, we want to make sure that .children has the total # of potential
# children, and that get_child_descriptors() returns the actual children
# chosen for a given student.
# In order to be able to call get_child_descriptors(), we must first
# call bind_for_student:
lc_block.bind_for_student(self._create_module_system(course), lc_block._field_data) # pylint: disable=protected-access
self.assertEqual(len(lc_block.children), num_to_create)
self.assertEqual(len(lc_block.get_child_descriptors()), num_expected)

def test_consistent_children(self):
"""
Test that the same student will always see the same selected child block
"""
# Create many blocks in the library and add them to a course:
for num in range(0, 8):
ItemFactory.create(
metadata={"data": "This is #{}".format(num + 1)},
category="html", parent_location=self.library.location, user_id=self.user.id, publish_item=False
)

with modulestore().default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()
module_system = self._create_module_system(course)

lc_block = self._add_library_content_block(course, self.lib_key, {'max_count': 1})
lc_block_key = lc_block.location
lc_block = self._refresh_children(lc_block)

def get_child_of_lc_block(block):
"""
Helper that gets the actual child block seen by a student.
We cannot use get_child_descriptors because it uses features that
are mocked by the test runtime.
"""
block_ids = list(block._xmodule.selected_children()) # pylint: disable=protected-access
self.assertEqual(len(block_ids), 1)
for child_key in block.children:
if child_key.block_id == block_ids[0]:
return modulestore().get_item(child_key)

# bind the module for a student:
lc_block.bind_for_student(module_system, lc_block._field_data) # pylint: disable=protected-access
chosen_child = get_child_of_lc_block(lc_block)
chosen_child_defn_id = chosen_child.definition_locator.definition_id

modulestore().update_item(lc_block, self.user.id)

# Now re-load the block and try again:
def check():
"""
Confirm that chosen_child is still the child seen by the test student
"""
for _ in range(0, 10): # Repeat many times b/c blocks are randomized
lc_block = modulestore().get_item(lc_block_key) # Reload block from the database
lc_block.bind_for_student(module_system, lc_block._field_data) # pylint: disable=protected-access
current_child = get_child_of_lc_block(lc_block)
self.assertEqual(current_child.location, chosen_child.location)
self.assertEqual(current_child.data, chosen_child.data)
self.assertEqual(current_child.definition_locator.definition_id, chosen_child_defn_id)
check()

# Refresh the children:
lc_block = self._refresh_children(lc_block)
lc_block.bind_for_student(module_system, lc_block._field_data) # pylint: disable=protected-access

# Now re-load the block and try yet again, in case refreshing the children changed anything:
check()

def test_definition_shared_with_library(self):
"""
Test that the same block definition is used for the library and course[s]
"""
block1 = ItemFactory.create(category="html", parent_location=self.library.location, user_id=self.user.id, publish_item=False)
def_id1 = block1.definition_locator.definition_id
block2 = ItemFactory.create(category="html", parent_location=self.library.location, user_id=self.user.id, publish_item=False)
def_id2 = block2.definition_locator.definition_id
self.assertNotEqual(def_id1, def_id2)

# Next, create a course:
with modulestore().default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()

# Add a LibraryContent block to the course:
lc_block = self._add_library_content_block(course, self.lib_key)
lc_block = self._refresh_children(lc_block)
for child_key in lc_block.children:
child = modulestore().get_item(child_key)
def_id = child.definition_locator.definition_id
self.assertIn(def_id, (def_id1, def_id2))

def test_fields(self):
"""
Test that blocks used from a library have the same field values as
defined by the library author.
"""
data_value = "A Scope.content value"
name_value = "A Scope.settings value"
lib_block = ItemFactory.create(
category="html",
parent_location=self.library.location,
user_id=self.user.id,
publish_item=False,
display_name=name_value,
metadata={
"data": data_value,
},
)
self.assertEqual(lib_block.data, data_value)
self.assertEqual(lib_block.display_name, name_value)

# Next, create a course:
with modulestore().default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()

# Add a LibraryContent block to the course:
lc_block = self._add_library_content_block(course, self.lib_key)
lc_block = self._refresh_children(lc_block)
course_block = modulestore().get_item(lc_block.children[0])

self.assertEqual(course_block.data, data_value)
self.assertEqual(course_block.display_name, name_value)

def test_block_with_children(self):
"""
Test that blocks used from a library can have children.
"""
data_value = "A Scope.content value"
name_value = "A Scope.settings value"
# In the library, create a vertical block with a child:
vert_block = ItemFactory.create(
category="vertical",
parent_location=self.library.location,
user_id=self.user.id,
publish_item=False,
)
child_block = ItemFactory.create(
category="html",
parent_location=vert_block.location,
user_id=self.user.id,
publish_item=False,
display_name=name_value,
metadata={"data": data_value, },
)
self.assertEqual(child_block.data, data_value)
self.assertEqual(child_block.display_name, name_value)

# Next, create a course:
with modulestore().default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()

# Add a LibraryContent block to the course:
lc_block = self._add_library_content_block(course, self.lib_key)
lc_block = self._refresh_children(lc_block)
self.assertEqual(len(lc_block.children), 1)
course_vert_block = modulestore().get_item(lc_block.children[0])
self.assertEqual(len(course_vert_block.children), 1)
course_child_block = modulestore().get_item(course_vert_block.children[0])

self.assertEqual(course_child_block.data, data_value)
self.assertEqual(course_child_block.display_name, name_value)
7 changes: 7 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ def reverse_course_url(handler_name, course_key, kwargs=None):
return reverse_url(handler_name, 'course_key_string', course_key, kwargs)


def reverse_library_url(handler_name, library_key, kwargs=None):
"""
Creates the URL for handlers that use library_keys as URL parameters.
"""
return reverse_url(handler_name, 'library_key_string', library_key, kwargs)


def reverse_usage_url(handler_name, usage_key, kwargs=None):
"""
Creates the URL for handlers that use usage_keys as URL parameters.
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .helpers import *
from .item import *
from .import_export import *
from .library import *
from .preview import *
from .public import *
from .export_git import *
Expand Down
26 changes: 26 additions & 0 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
add_extra_panel_tab,
remove_extra_panel_tab,
reverse_course_url,
reverse_library_url,
reverse_usage_url,
reverse_url,
remove_all_instructors,
Expand All @@ -56,6 +57,7 @@
ADVANCED_COMPONENT_TYPES,
)
from contentstore.tasks import rerun_course
from .library import LIBRARIES_ENABLED
from .item import create_xblock_info
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils
Expand Down Expand Up @@ -341,6 +343,14 @@ def _accessible_courses_list_from_groups(request):
return courses_list.values(), in_process_course_actions


def _accessible_libraries_list(user):
"""
List all libraries available to the logged in user by iterating through all libraries
"""
# No need to worry about ErrorDescriptors - split's get_libraries() never returns them.
return [lib for lib in modulestore().get_libraries() if has_course_access(user, lib.location)]


@login_required
@ensure_csrf_cookie
def course_listing(request):
Expand All @@ -360,6 +370,8 @@ def course_listing(request):
# so fallback to iterating through all courses
courses, in_process_course_actions = _accessible_courses_list(request)

libraries = _accessible_libraries_list(request.user) if LIBRARIES_ENABLED else []

def format_course_for_view(course):
"""
Return a dict of the data which the view requires for each course
Expand Down Expand Up @@ -393,6 +405,18 @@ def format_in_process_course_view(uca):
}) if uca.state == CourseRerunUIStateManager.State.FAILED else ''
}

def format_library_for_view(library):
"""
Return a dict of the data which the view requires for each library
"""
return {
'display_name': library.display_name,
'library_key': unicode(library.location.library_key),
'url': reverse_library_url('library_handler', unicode(library.location.library_key)),
'org': library.display_org_with_default,
'number': library.display_number_with_default,
}

# remove any courses in courses that are also in the in_process_course_actions list
in_process_action_course_keys = [uca.course_key for uca in in_process_course_actions]
courses = [
Expand All @@ -406,6 +430,8 @@ def format_in_process_course_view(uca):
return render_to_response('index.html', {
'courses': courses,
'in_process_course_actions': in_process_course_actions,
'libraries_enabled': LIBRARIES_ENABLED,
'libraries': [format_library_for_view(lib) for lib in libraries],
'user': request.user,
'request_course_creator_url': reverse('contentstore.views.request_course_creator'),
'course_creator_status': _get_course_creator_status(request.user),
Expand Down
5 changes: 4 additions & 1 deletion cms/djangoapps/contentstore/views/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from edxmako.shortcuts import render_to_string, render_to_response
from xblock.core import XBlock
from xmodule.modulestore.django import modulestore
from contentstore.utils import reverse_course_url, reverse_usage_url
from contentstore.utils import reverse_course_url, reverse_library_url, reverse_usage_url

__all__ = ['edge', 'event', 'landing']

Expand Down Expand Up @@ -106,6 +106,9 @@ def xblock_studio_url(xblock, parent_xblock=None):
url=reverse_course_url('course_handler', xblock.location.course_key),
usage_key=urllib.quote(unicode(xblock.location))
)
elif category == 'library':
library_key = xblock.location.course_key
return reverse_library_url('library_handler', library_key)
else:
return reverse_usage_url('container_handler', xblock.location)

Expand Down
Loading