Show a select box when editing a library content block
This commit is contained in:
@@ -13,7 +13,6 @@ from student.roles import (
|
||||
OrgStaffRole, OrgInstructorRole, OrgLibraryUserRole,
|
||||
)
|
||||
from xblock.reference.user_service import XBlockUser
|
||||
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
|
||||
@@ -64,7 +63,7 @@ class LibraryTestCase(ModuleStoreTestCase):
|
||||
parent_location=course.location,
|
||||
user_id=self.user.id,
|
||||
publish_item=False,
|
||||
source_libraries=[LibraryVersionReference(library_key)],
|
||||
source_library_id=unicode(library_key),
|
||||
**(other_settings or {})
|
||||
)
|
||||
|
||||
@@ -333,7 +332,7 @@ class TestLibraries(LibraryTestCase):
|
||||
# Now, change the block settings to have an invalid library key:
|
||||
resp = self._update_item(
|
||||
lc_block.location,
|
||||
{"source_libraries": [["library-v1:NOT+FOUND", None]]},
|
||||
{"source_library_id": "library-v1:NOT+FOUND"},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
lc_block = modulestore().get_item(lc_block.location)
|
||||
@@ -376,7 +375,7 @@ class TestLibraries(LibraryTestCase):
|
||||
# Now, change the block settings to have an invalid library key:
|
||||
resp = self._update_item(
|
||||
lc_block.location,
|
||||
{"source_libraries": [[str(library2key)]]},
|
||||
{"source_library_id": str(library2key)},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
lc_block = modulestore().get_item(lc_block.location)
|
||||
@@ -450,7 +449,7 @@ class TestLibraries(LibraryTestCase):
|
||||
# Now, change the block settings to have an invalid library key:
|
||||
resp = self._update_item(
|
||||
lc_block.location,
|
||||
{"source_libraries": [["library-v1:NOT+FOUND", None]]},
|
||||
{"source_library_id": "library-v1:NOT+FOUND"},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
with self.assertRaises(ValueError):
|
||||
|
||||
@@ -72,23 +72,13 @@ from django.utils.translation import ugettext as _
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complementary">
|
||||
<div class="library-location">
|
||||
<h4 class="bar-mod-title">${_("Library ID")}</h4>
|
||||
<div class="wrapper-library-id bar-mod-content">
|
||||
<h5 class="title">${_("Library ID")}</h5>
|
||||
<p class="library-id">
|
||||
<span class="library-id-value">${context_library.location.library_key | h}</span>
|
||||
<span class="tip"><span class="sr">${_("Tip:")}</span> ${_("To add content from this library to a course that uses a Randomized Content Block, copy this ID and enter it in the Libraries field in the Randomized Content Block settings.")}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
% if can_edit:
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Adding content to your library")}</h3>
|
||||
<p>${_("Add components to your library for use in courses, using Add New Component at the bottom of this page.")}</p>
|
||||
<p>${_("Components are listed in the order in which they are added, with the most recently added at the bottom. Use the pagination arrows to navigate from page to page if you have more than one page of components in your library.")}</p>
|
||||
<h3 class="title-3">${_("Using library content in courses")}</h3>
|
||||
<p>${_("Use library content in courses by adding the {em_start}library_content{em_end} policy key to Advanced Settings, then adding a Randomized Content Block to your courseware. In the settings for each Randomized Content Block, enter the Library ID for each library from which you want to draw content, and specify the number of problems to be randomly selected and displayed to each student.").format(em_start='<strong>', em_end="</strong>")}</p>
|
||||
<p>${_("Use library content in courses by adding the {em_start}library_content{em_end} policy key to the Advanced Module List in the course's Advanced Settings, then adding a Randomized Content Block to your courseware. In the settings for each Randomized Content Block, select this library as the source library, and specify the number of problems to be randomly selected and displayed to each student.").format(em_start='<strong>', em_end="</strong>")}</p>
|
||||
</div>
|
||||
% endif
|
||||
<div class="bit external-help">
|
||||
|
||||
@@ -9,7 +9,6 @@ from capa.responsetypes import registry
|
||||
from gettext import ngettext
|
||||
|
||||
from .mako_module import MakoModuleDescriptor
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
import random
|
||||
from webob import Response
|
||||
@@ -30,11 +29,6 @@ _ = lambda text: text
|
||||
ANY_CAPA_TYPE_VALUE = 'any'
|
||||
|
||||
|
||||
def enum(**enums):
|
||||
""" enum helper in lieu of enum34 """
|
||||
return type('Enum', (), enums)
|
||||
|
||||
|
||||
def _get_human_name(problem_class):
|
||||
"""
|
||||
Get the human-friendly name for a problem type.
|
||||
@@ -54,85 +48,6 @@ def _get_capa_types():
|
||||
], key=lambda item: item.get('display_name'))
|
||||
|
||||
|
||||
class LibraryVersionReference(namedtuple("LibraryVersionReference", "library_id version")):
|
||||
"""
|
||||
A reference to a specific library, with an optional version.
|
||||
The version is used to find out when the LibraryContentXBlock was last
|
||||
updated with the latest content from the library.
|
||||
|
||||
library_id is a LibraryLocator
|
||||
version is an ObjectId or None
|
||||
"""
|
||||
def __new__(cls, library_id, version=None):
|
||||
# pylint: disable=super-on-old-class
|
||||
if not isinstance(library_id, LibraryLocator):
|
||||
library_id = LibraryLocator.from_string(library_id)
|
||||
if library_id.version_guid:
|
||||
assert (version is None) or (version == library_id.version_guid)
|
||||
if not version:
|
||||
version = library_id.version_guid
|
||||
library_id = library_id.for_version(None)
|
||||
if version and not isinstance(version, ObjectId):
|
||||
try:
|
||||
version = ObjectId(version)
|
||||
except InvalidId:
|
||||
raise ValueError(version)
|
||||
return super(LibraryVersionReference, cls).__new__(cls, library_id, version)
|
||||
|
||||
@staticmethod
|
||||
def from_json(value):
|
||||
"""
|
||||
Implement from_json to convert from JSON
|
||||
"""
|
||||
return LibraryVersionReference(*value)
|
||||
|
||||
def to_json(self):
|
||||
"""
|
||||
Implement to_json to convert value to JSON
|
||||
"""
|
||||
# TODO: Is there anyway for an xblock to *store* an ObjectId as
|
||||
# part of the List() field value?
|
||||
return [unicode(self.library_id), unicode(self.version) if self.version else None] # pylint: disable=no-member
|
||||
|
||||
|
||||
class LibraryList(List):
|
||||
"""
|
||||
Special List class for listing references to content libraries.
|
||||
Is simply a list of LibraryVersionReference tuples.
|
||||
"""
|
||||
def from_json(self, values):
|
||||
"""
|
||||
Implement from_json to convert from JSON.
|
||||
|
||||
values might be a list of lists, or a list of strings
|
||||
Normally the runtime gives us:
|
||||
[[u'library-v1:ProblemX+PR0B', '5436ffec56c02c13806a4c1b'], ...]
|
||||
But the studio editor gives us:
|
||||
[u'library-v1:ProblemX+PR0B,5436ffec56c02c13806a4c1b', ...]
|
||||
"""
|
||||
def parse(val):
|
||||
""" Convert this list entry from its JSON representation """
|
||||
if isinstance(val, basestring):
|
||||
val = val.strip(' []')
|
||||
parts = val.rsplit(',', 1)
|
||||
val = [parts[0], parts[1] if len(parts) > 1 else None]
|
||||
try:
|
||||
return LibraryVersionReference.from_json(val)
|
||||
except InvalidKeyError:
|
||||
try:
|
||||
friendly_val = val[0] # Just get the library key part, not the version
|
||||
except IndexError:
|
||||
friendly_val = unicode(val)
|
||||
raise ValueError(_('"{value}" is not a valid library ID.').format(value=friendly_val))
|
||||
return [parse(v) for v in values]
|
||||
|
||||
def to_json(self, values):
|
||||
"""
|
||||
Implement to_json to convert value to JSON
|
||||
"""
|
||||
return [lvr.to_json() for lvr in values]
|
||||
|
||||
|
||||
class LibraryContentFields(object):
|
||||
"""
|
||||
Fields for the LibraryContentModule.
|
||||
@@ -141,7 +56,7 @@ class LibraryContentFields(object):
|
||||
descriptor.
|
||||
"""
|
||||
# Please note the display_name of each field below is used in
|
||||
# common/test/acceptance/pages/studio/overview.py:StudioLibraryContentXBlockEditModal
|
||||
# common/test/acceptance/pages/studio/library.py:StudioLibraryContentXBlockEditModal
|
||||
# to locate input elements - keep synchronized
|
||||
display_name = String(
|
||||
display_name=_("Display Name"),
|
||||
@@ -149,10 +64,15 @@ class LibraryContentFields(object):
|
||||
default="Randomized Content Block",
|
||||
scope=Scope.settings,
|
||||
)
|
||||
source_libraries = LibraryList(
|
||||
display_name=_("Libraries"),
|
||||
help=_("Enter a library ID for each library from which you want to draw content."),
|
||||
default=[],
|
||||
source_library_id = String(
|
||||
display_name=_("Library"),
|
||||
help=_("Select the library from which you want to draw content."),
|
||||
scope=Scope.settings,
|
||||
values_provider=lambda instance: instance.source_library_values(),
|
||||
)
|
||||
source_library_version = String(
|
||||
# This is a hidden field that stores the version of source_library when we last pulled content from it
|
||||
display_name=_("Library Version"),
|
||||
scope=Scope.settings,
|
||||
)
|
||||
mode = String(
|
||||
@@ -194,6 +114,13 @@ class LibraryContentFields(object):
|
||||
)
|
||||
has_children = True
|
||||
|
||||
@property
|
||||
def source_library_key(self):
|
||||
"""
|
||||
Convenience method to get the library ID as a LibraryLocator and not just a string
|
||||
"""
|
||||
return LibraryLocator.from_string(self.source_library_id)
|
||||
|
||||
|
||||
#pylint: disable=abstract-method
|
||||
@XBlock.wants('library_tools') # Only needed in studio
|
||||
@@ -361,7 +288,7 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
|
||||
# The only supported mode is currently 'random'.
|
||||
# Add the mode field to non_editable_metadata_fields so that it doesn't
|
||||
# render in the edit form.
|
||||
non_editable_fields.append(LibraryContentFields.mode)
|
||||
non_editable_fields.extend([LibraryContentFields.mode, LibraryContentFields.source_library_version])
|
||||
return non_editable_fields
|
||||
|
||||
@XBlock.handler
|
||||
@@ -374,7 +301,7 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
|
||||
will be given new block_ids, but the definition ID used should be the
|
||||
exact same definition ID used in the library.
|
||||
|
||||
This method will update this block's 'source_libraries' field to store
|
||||
This method will update this block's 'source_library_id' field to store
|
||||
the version number of the libraries used, so we easily determine if
|
||||
this block is up to date or not.
|
||||
"""
|
||||
@@ -395,7 +322,7 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
|
||||
"""
|
||||
latest_version = lib_tools.get_library_version(library_key)
|
||||
if latest_version is not None:
|
||||
if version is None or version != latest_version:
|
||||
if version is None or version != unicode(latest_version):
|
||||
validation.set_summary(
|
||||
StudioValidationMessage(
|
||||
StudioValidationMessage.WARNING,
|
||||
@@ -446,7 +373,7 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
|
||||
)
|
||||
)
|
||||
return validation
|
||||
if not self.source_libraries:
|
||||
if not self.source_library_id:
|
||||
validation.set_summary(
|
||||
StudioValidationMessage(
|
||||
StudioValidationMessage.NOT_CONFIGURED,
|
||||
@@ -457,12 +384,10 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
|
||||
)
|
||||
return validation
|
||||
lib_tools = self.runtime.service(self, 'library_tools')
|
||||
for library_key, version in self.source_libraries:
|
||||
if not self._validate_library_version(validation, lib_tools, version, library_key):
|
||||
break
|
||||
self._validate_library_version(validation, lib_tools, self.source_library_version, self.source_library_key)
|
||||
|
||||
# Note: we assume refresh_children() has been called
|
||||
# since the last time fields like source_libraries or capa_types were changed.
|
||||
# since the last time fields like source_library_id or capa_types were changed.
|
||||
matching_children_count = len(self.children) # pylint: disable=no-member
|
||||
if matching_children_count == 0:
|
||||
self._set_validation_error_if_empty(
|
||||
@@ -499,12 +424,31 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
|
||||
|
||||
return validation
|
||||
|
||||
def source_library_values(self):
|
||||
"""
|
||||
Return a list of possible values for self.source_library_id
|
||||
"""
|
||||
lib_tools = self.runtime.service(self, 'library_tools')
|
||||
user_perms = self.runtime.service(self, 'studio_user_permissions')
|
||||
all_libraries = lib_tools.list_available_libraries()
|
||||
if user_perms:
|
||||
all_libraries = [
|
||||
(key, name) for key, name in all_libraries
|
||||
if user_perms.can_read(key) or self.source_library_id == unicode(key)
|
||||
]
|
||||
all_libraries.sort(key=lambda entry: entry[1]) # Sort by name
|
||||
if self.source_library_id and self.source_library_key not in [entry[0] for entry in all_libraries]:
|
||||
all_libraries.append((self.source_library_id, _(u"Invalid Library")))
|
||||
all_libraries = [(u"", _("No Library Selected"))] + all_libraries
|
||||
values = [{"display_name": name, "value": unicode(key)} for key, name in all_libraries]
|
||||
return values
|
||||
|
||||
def editor_saved(self, user, old_metadata, old_content):
|
||||
"""
|
||||
If source_libraries or capa_type has been edited, refresh_children automatically.
|
||||
If source_library_id or capa_type has been edited, refresh_children automatically.
|
||||
"""
|
||||
old_source_libraries = LibraryList().from_json(old_metadata.get('source_libraries', []))
|
||||
if (set(old_source_libraries) != set(self.source_libraries) or
|
||||
old_source_library_id = old_metadata.get('source_library_id', [])
|
||||
if (old_source_library_id != self.source_library_id or
|
||||
old_metadata.get('capa_type', ANY_CAPA_TYPE_VALUE) != self.capa_type):
|
||||
try:
|
||||
self.refresh_children()
|
||||
|
||||
@@ -3,7 +3,7 @@ XBlock runtime services for LibraryContentModule
|
||||
"""
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
from xmodule.library_content_module import LibraryVersionReference, ANY_CAPA_TYPE_VALUE
|
||||
from xmodule.library_content_module import ANY_CAPA_TYPE_VALUE
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
|
||||
@@ -117,25 +117,37 @@ class LibraryToolsService(object):
|
||||
if user_perms and not user_perms.can_write(dest_block.location.course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
new_libraries = []
|
||||
if not dest_block.source_library_id:
|
||||
dest_block.source_library_version = ""
|
||||
return
|
||||
|
||||
source_blocks = []
|
||||
for library_key, __ in dest_block.source_libraries:
|
||||
library = self._get_library(library_key)
|
||||
if library is None:
|
||||
raise ValueError("Required library not found.")
|
||||
if user_perms and not user_perms.can_read(library_key):
|
||||
raise PermissionDenied()
|
||||
filter_children = (dest_block.capa_type != ANY_CAPA_TYPE_VALUE)
|
||||
if filter_children:
|
||||
# Apply simple filtering based on CAPA problem types:
|
||||
source_blocks.extend([key for key in library.children if self._filter_child(key, dest_block.capa_type)])
|
||||
else:
|
||||
source_blocks.extend(library.children)
|
||||
new_libraries.append(LibraryVersionReference(library_key, library.location.library_key.version_guid))
|
||||
library_key = dest_block.source_library_key
|
||||
library = self._get_library(library_key)
|
||||
if library is None:
|
||||
raise ValueError("Required library not found.")
|
||||
if user_perms and not user_perms.can_read(library_key):
|
||||
raise PermissionDenied()
|
||||
filter_children = (dest_block.capa_type != ANY_CAPA_TYPE_VALUE)
|
||||
if filter_children:
|
||||
# Apply simple filtering based on CAPA problem types:
|
||||
source_blocks.extend([key for key in library.children if self._filter_child(key, dest_block.capa_type)])
|
||||
else:
|
||||
source_blocks.extend(library.children)
|
||||
|
||||
with self.store.bulk_operations(dest_block.location.course_key):
|
||||
dest_block.source_libraries = new_libraries
|
||||
dest_block.source_library_version = unicode(library.location.library_key.version_guid)
|
||||
self.store.update_item(dest_block, user_id)
|
||||
dest_block.children = self.store.copy_from_template(source_blocks, dest_block.location, user_id)
|
||||
# ^-- copy_from_template updates the children in the DB
|
||||
# but we must also set .children here to avoid overwriting the DB again
|
||||
|
||||
def list_available_libraries(self):
|
||||
"""
|
||||
List all known libraries.
|
||||
Returns tuples of (LibraryLocator, display_name)
|
||||
"""
|
||||
return [
|
||||
(lib.location.library_key.replace(version_guid=None, branch=None), lib.display_name)
|
||||
for lib in self.store.get_libraries()
|
||||
]
|
||||
|
||||
@@ -6,15 +6,11 @@ Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`.
|
||||
"""
|
||||
from bson.objectid import ObjectId
|
||||
from mock import Mock, patch
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
from unittest import TestCase
|
||||
|
||||
from xblock.fragment import Fragment
|
||||
from xblock.runtime import Runtime as VanillaRuntime
|
||||
|
||||
from xmodule.library_content_module import (
|
||||
LibraryVersionReference, LibraryList, ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor
|
||||
)
|
||||
from xmodule.library_content_module import ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor
|
||||
from xmodule.library_tools import LibraryToolsService
|
||||
from xmodule.modulestore.tests.factories import LibraryFactory, CourseFactory
|
||||
from xmodule.modulestore.tests.utils import MixedSplitTestCase
|
||||
@@ -46,7 +42,7 @@ class LibraryContentTest(MixedSplitTestCase):
|
||||
"library_content",
|
||||
self.vertical,
|
||||
max_count=1,
|
||||
source_libraries=[LibraryVersionReference(self.library.location.library_key)]
|
||||
source_library_id=unicode(self.library.location.library_key)
|
||||
)
|
||||
|
||||
def _bind_course_module(self, module):
|
||||
@@ -128,17 +124,17 @@ class TestLibraryContentModule(LibraryContentTest):
|
||||
def test_validation_of_course_libraries(self):
|
||||
"""
|
||||
Test that the validation method of LibraryContent blocks can validate
|
||||
the source_libraries setting.
|
||||
the source_library setting.
|
||||
"""
|
||||
# When source_libraries is blank, the validation summary should say this block needs to be configured:
|
||||
self.lc_block.source_libraries = []
|
||||
self.lc_block.source_library_id = ""
|
||||
result = self.lc_block.validate()
|
||||
self.assertFalse(result) # Validation fails due to at least one warning/message
|
||||
self.assertTrue(result.summary)
|
||||
self.assertEqual(StudioValidationMessage.NOT_CONFIGURED, result.summary.type)
|
||||
|
||||
# When source_libraries references a non-existent library, we should get an error:
|
||||
self.lc_block.source_libraries = [LibraryVersionReference("library-v1:BAD+WOLF")]
|
||||
self.lc_block.source_library_id = "library-v1:BAD+WOLF"
|
||||
result = self.lc_block.validate()
|
||||
self.assertFalse(result) # Validation fails due to at least one warning/message
|
||||
self.assertTrue(result.summary)
|
||||
@@ -146,7 +142,7 @@ class TestLibraryContentModule(LibraryContentTest):
|
||||
self.assertIn("invalid", result.summary.text)
|
||||
|
||||
# When source_libraries is set but the block needs to be updated, the summary should say so:
|
||||
self.lc_block.source_libraries = [LibraryVersionReference(self.library.location.library_key)]
|
||||
self.lc_block.source_library_id = unicode(self.library.location.library_key)
|
||||
result = self.lc_block.validate()
|
||||
self.assertFalse(result) # Validation fails due to at least one warning/message
|
||||
self.assertTrue(result.summary)
|
||||
@@ -268,47 +264,6 @@ class TestLibraryContentRender(LibraryContentTest):
|
||||
self.assertEqual("LibraryContentAuthorView", rendered.js_init_fn) # but some js initialization should happen
|
||||
|
||||
|
||||
class TestLibraryList(TestCase):
|
||||
""" Tests for LibraryList XBlock Field """
|
||||
def test_from_json_runtime_style(self):
|
||||
"""
|
||||
Test that LibraryList can parse raw libraries list as passed by runtime
|
||||
"""
|
||||
lib_list = LibraryList()
|
||||
lib1_key, lib1_version = u'library-v1:Org1+Lib1', '5436ffec56c02c13806a4c1b'
|
||||
lib2_key, lib2_version = u'library-v1:Org2+Lib2', '112dbaf312c0daa019ce9992'
|
||||
raw = [[lib1_key, lib1_version], [lib2_key, lib2_version]]
|
||||
parsed = lib_list.from_json(raw)
|
||||
self.assertEqual(len(parsed), 2)
|
||||
self.assertEquals(parsed[0].library_id, LibraryLocator.from_string(lib1_key))
|
||||
self.assertEquals(parsed[0].version, ObjectId(lib1_version))
|
||||
self.assertEquals(parsed[1].library_id, LibraryLocator.from_string(lib2_key))
|
||||
self.assertEquals(parsed[1].version, ObjectId(lib2_version))
|
||||
|
||||
def test_from_json_studio_editor_style(self):
|
||||
"""
|
||||
Test that LibraryList can parse raw libraries list as passed by studio editor
|
||||
"""
|
||||
lib_list = LibraryList()
|
||||
lib1_key, lib1_version = u'library-v1:Org1+Lib1', '5436ffec56c02c13806a4c1b'
|
||||
lib2_key, lib2_version = u'library-v1:Org2+Lib2', '112dbaf312c0daa019ce9992'
|
||||
raw = [lib1_key + ',' + lib1_version, lib2_key + ',' + lib2_version]
|
||||
parsed = lib_list.from_json(raw)
|
||||
self.assertEqual(len(parsed), 2)
|
||||
self.assertEquals(parsed[0].library_id, LibraryLocator.from_string(lib1_key))
|
||||
self.assertEquals(parsed[0].version, ObjectId(lib1_version))
|
||||
self.assertEquals(parsed[1].library_id, LibraryLocator.from_string(lib2_key))
|
||||
self.assertEquals(parsed[1].version, ObjectId(lib2_version))
|
||||
|
||||
def test_from_json_invalid_value(self):
|
||||
"""
|
||||
Test that LibraryList raises Value error if invalid library key is given
|
||||
"""
|
||||
lib_list = LibraryList()
|
||||
with self.assertRaises(ValueError):
|
||||
lib_list.from_json(["Not-a-library-key,whatever"])
|
||||
|
||||
|
||||
class TestLibraryContentAnalytics(LibraryContentTest):
|
||||
"""
|
||||
Test analytics features of LibraryContentModule
|
||||
|
||||
@@ -1003,6 +1003,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
# 3. A generic string editor for anything else (editing JSON representation of the value).
|
||||
editor_type = "Generic"
|
||||
values = field.values
|
||||
if "values_provider" in field.runtime_options:
|
||||
values = field.runtime_options['values_provider'](self)
|
||||
if isinstance(values, (tuple, list)) and len(values) > 0:
|
||||
editor_type = "Select"
|
||||
values = [jsonify_value(field, json_choice) for json_choice in values]
|
||||
|
||||
@@ -4,9 +4,8 @@ Library edit page in Studio
|
||||
from bok_choy.javascript import js_defined, wait_for_js
|
||||
from bok_choy.page_object import PageObject
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support.select import Select
|
||||
from .overview import CourseOutlineModal
|
||||
from .component_editor import ComponentEditorView
|
||||
from .container import XBlockWrapper
|
||||
from ...pages.studio.pagination import PaginatedMixin
|
||||
from ...tests.helpers import disable_animations
|
||||
@@ -111,65 +110,41 @@ class LibraryPage(PageObject, PaginatedMixin):
|
||||
)
|
||||
|
||||
|
||||
class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
|
||||
class StudioLibraryContentEditor(ComponentEditorView):
|
||||
"""
|
||||
Library Content XBlock Modal edit window
|
||||
"""
|
||||
url = None
|
||||
MODAL_SELECTOR = ".wrapper-modal-window-edit-xblock"
|
||||
|
||||
# Labels used to identify the fields on the edit modal:
|
||||
LIBRARY_LABEL = "Libraries"
|
||||
LIBRARY_LABEL = "Library"
|
||||
COUNT_LABEL = "Count"
|
||||
SCORED_LABEL = "Scored"
|
||||
PROBLEM_TYPE_LABEL = "Problem Type"
|
||||
|
||||
def is_browser_on_page(self):
|
||||
"""
|
||||
Check that we are on the right page in the browser.
|
||||
"""
|
||||
return self.is_shown()
|
||||
|
||||
@property
|
||||
def library_key(self):
|
||||
"""
|
||||
Gets value of first library key input
|
||||
"""
|
||||
library_key_input = self.get_metadata_input(self.LIBRARY_LABEL)
|
||||
if library_key_input is not None:
|
||||
return library_key_input.get_attribute('value').strip(',')
|
||||
return None
|
||||
def library_name(self):
|
||||
return self.get_selected_option_text(self.LIBRARY_LABEL)
|
||||
|
||||
@library_key.setter
|
||||
def library_key(self, library_key):
|
||||
@library_name.setter
|
||||
def library_name(self, library_name):
|
||||
"""
|
||||
Sets value of first library key input, creating it if necessary
|
||||
Select a library from the library select box
|
||||
"""
|
||||
library_key_input = self.get_metadata_input(self.LIBRARY_LABEL)
|
||||
if library_key_input is None:
|
||||
library_key_input = self._add_library_key()
|
||||
if library_key is not None:
|
||||
# can't use lib_text.clear() here as input get deleted by client side script
|
||||
library_key_input.send_keys(Keys.HOME)
|
||||
library_key_input.send_keys(Keys.SHIFT, Keys.END)
|
||||
library_key_input.send_keys(library_key)
|
||||
else:
|
||||
library_key_input.clear()
|
||||
EmptyPromise(lambda: self.library_key == library_key, "library_key is updated in modal.").fulfill()
|
||||
self.set_select_value(self.LIBRARY_LABEL, library_name)
|
||||
EmptyPromise(lambda: self.library_name == library_name, "library_name is updated in modal.").fulfill()
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
"""
|
||||
Gets value of children count input
|
||||
"""
|
||||
return int(self.get_metadata_input(self.COUNT_LABEL).get_attribute('value'))
|
||||
return int(self.get_setting_element(self.COUNT_LABEL).get_attribute('value'))
|
||||
|
||||
@count.setter
|
||||
def count(self, count):
|
||||
"""
|
||||
Sets value of children count input
|
||||
"""
|
||||
count_text = self.get_metadata_input(self.COUNT_LABEL)
|
||||
count_text = self.get_setting_element(self.COUNT_LABEL)
|
||||
count_text.clear()
|
||||
count_text.send_keys(count)
|
||||
EmptyPromise(lambda: self.count == count, "count is updated in modal.").fulfill()
|
||||
@@ -179,7 +154,7 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
|
||||
"""
|
||||
Gets value of scored select
|
||||
"""
|
||||
value = self.get_metadata_input(self.SCORED_LABEL).get_attribute('value')
|
||||
value = self.get_selected_option_text(self.SCORED_LABEL)
|
||||
if value == 'True':
|
||||
return True
|
||||
elif value == 'False':
|
||||
@@ -191,10 +166,7 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
|
||||
"""
|
||||
Sets value of scored select
|
||||
"""
|
||||
select_element = self.get_metadata_input(self.SCORED_LABEL)
|
||||
select_element.click()
|
||||
scored_select = Select(select_element)
|
||||
scored_select.select_by_value(str(scored))
|
||||
self.set_select_value(self.SCORED_LABEL, str(scored))
|
||||
EmptyPromise(lambda: self.scored == scored, "scored is updated in modal.").fulfill()
|
||||
|
||||
@property
|
||||
@@ -202,54 +174,23 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
|
||||
"""
|
||||
Gets value of CAPA type select
|
||||
"""
|
||||
return self.get_metadata_input(self.PROBLEM_TYPE_LABEL).get_attribute('value')
|
||||
return self.get_setting_element(self.PROBLEM_TYPE_LABEL).get_attribute('value')
|
||||
|
||||
@capa_type.setter
|
||||
def capa_type(self, value):
|
||||
"""
|
||||
Sets value of CAPA type select
|
||||
"""
|
||||
select_element = self.get_metadata_input(self.PROBLEM_TYPE_LABEL)
|
||||
select_element.click()
|
||||
problem_type_select = Select(select_element)
|
||||
problem_type_select.select_by_value(value)
|
||||
self.set_select_value(self.PROBLEM_TYPE_LABEL, value)
|
||||
EmptyPromise(lambda: self.capa_type == value, "problem type is updated in modal.").fulfill()
|
||||
|
||||
def _add_library_key(self):
|
||||
def set_select_value(self, label, value):
|
||||
"""
|
||||
Adds library key input
|
||||
Sets the select with given label (display name) to the specified value
|
||||
"""
|
||||
wrapper = self._get_metadata_element(self.LIBRARY_LABEL)
|
||||
add_button = wrapper.find_element_by_xpath(".//a[contains(@class, 'create-action')]")
|
||||
add_button.click()
|
||||
return self._get_list_inputs(wrapper)[0]
|
||||
|
||||
def _get_list_inputs(self, list_wrapper):
|
||||
"""
|
||||
Finds nested input elements (useful for List and Dict fields)
|
||||
"""
|
||||
return list_wrapper.find_elements_by_xpath(".//input[@type='text']")
|
||||
|
||||
def _get_metadata_element(self, metadata_key):
|
||||
"""
|
||||
Gets metadata input element (a wrapper div for List and Dict fields)
|
||||
"""
|
||||
metadata_inputs = self.find_css(".metadata_entry .wrapper-comp-setting label.setting-label")
|
||||
target_label = [elem for elem in metadata_inputs if elem.text == metadata_key][0]
|
||||
label_for = target_label.get_attribute('for')
|
||||
return self.find_css("#" + label_for)[0]
|
||||
|
||||
def get_metadata_input(self, metadata_key):
|
||||
"""
|
||||
Gets input/select element for given field
|
||||
"""
|
||||
element = self._get_metadata_element(metadata_key)
|
||||
if element.tag_name == 'div':
|
||||
# List or Dict field - return first input
|
||||
# TODO support multiple values
|
||||
inputs = self._get_list_inputs(element)
|
||||
element = inputs[0] if inputs else None
|
||||
return element
|
||||
elem = self.get_setting_element(label)
|
||||
select = Select(elem)
|
||||
select.select_by_value(value)
|
||||
|
||||
|
||||
@js_defined('window.LibraryContentAuthorView')
|
||||
|
||||
@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr
|
||||
from ..helpers import UniqueCourseTest
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...pages.studio.library import StudioLibraryContentXBlockEditModal, StudioLibraryContainerXBlockWrapper
|
||||
from ...pages.studio.library import StudioLibraryContentEditor, StudioLibraryContainerXBlockWrapper
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
from ...pages.lms.library import LibraryContentXBlockWrapper
|
||||
from ...pages.common.logout import LogoutPage
|
||||
@@ -65,7 +65,7 @@ class LibraryContentTestBase(UniqueCourseTest):
|
||||
)
|
||||
|
||||
library_content_metadata = {
|
||||
'source_libraries': [self.library_key],
|
||||
'source_library_id': unicode(self.library_key),
|
||||
'mode': 'random',
|
||||
'max_count': 1,
|
||||
'has_score': False
|
||||
@@ -90,12 +90,13 @@ class LibraryContentTestBase(UniqueCourseTest):
|
||||
Performs library block refresh in Studio, configuring it to show {count} children
|
||||
"""
|
||||
unit_page = self._go_to_unit_page(True)
|
||||
library_container_block = StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(unit_page.xblocks[0])
|
||||
modal = StudioLibraryContentXBlockEditModal(library_container_block.edit())
|
||||
modal.count = count
|
||||
library_container_block = StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(unit_page.xblocks[1])
|
||||
library_container_block.edit()
|
||||
editor = StudioLibraryContentEditor(self.browser, library_container_block.locator)
|
||||
editor.count = count
|
||||
if capa_type is not None:
|
||||
modal.capa_type = capa_type
|
||||
library_container_block.save_settings()
|
||||
editor.capa_type = capa_type
|
||||
editor.save()
|
||||
self._go_to_unit_page(change_login=False)
|
||||
unit_page.wait_for_page()
|
||||
unit_page.publish_action.click()
|
||||
|
||||
@@ -4,12 +4,11 @@ Acceptance tests for Library Content in LMS
|
||||
import ddt
|
||||
from flaky import flaky
|
||||
import textwrap
|
||||
from unittest import skip
|
||||
|
||||
from .base_studio_test import StudioLibraryTest
|
||||
from ...fixtures.course import CourseFixture
|
||||
from ..helpers import UniqueCourseTest
|
||||
from ...pages.studio.library import StudioLibraryContentXBlockEditModal, StudioLibraryContainerXBlockWrapper
|
||||
from ...pages.studio.library import StudioLibraryContentEditor, StudioLibraryContainerXBlockWrapper
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...fixtures.course import XBlockFixtureDesc
|
||||
|
||||
@@ -56,7 +55,7 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
|
||||
def populate_course_fixture(self, course_fixture):
|
||||
""" Install a course with sections/problems, tabs, updates, and handouts """
|
||||
library_content_metadata = {
|
||||
'source_libraries': [self.library_key],
|
||||
'source_library_id': unicode(self.library_key),
|
||||
'mode': 'random',
|
||||
'max_count': 1,
|
||||
'has_score': False
|
||||
@@ -79,29 +78,32 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
|
||||
return StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(xblock)
|
||||
|
||||
@ddt.data(
|
||||
('library-v1:111+111', 1, True),
|
||||
('library-v1:edX+L104', 2, False),
|
||||
('library-v1:OtherX+IDDQD', 3, True),
|
||||
(1, True),
|
||||
(2, False),
|
||||
(3, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_can_edit_metadata(self, library_key, max_count, scored):
|
||||
def test_can_edit_metadata(self, max_count, scored):
|
||||
"""
|
||||
Scenario: Given I have a library, a course and library content xblock in a course
|
||||
When I go to studio unit page for library content block
|
||||
And I edit library content metadata and save it
|
||||
Then I can ensure that data is persisted
|
||||
"""
|
||||
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
|
||||
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
|
||||
edit_modal.library_key = library_key
|
||||
library_name = self.library_info['display_name']
|
||||
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[1])
|
||||
library_container.edit()
|
||||
edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator)
|
||||
edit_modal.library_name = library_name
|
||||
edit_modal.count = max_count
|
||||
edit_modal.scored = scored
|
||||
|
||||
library_container.save_settings() # saving settings
|
||||
|
||||
# open edit window again to verify changes are persistent
|
||||
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
|
||||
self.assertEqual(edit_modal.library_key, library_key)
|
||||
library_container.edit()
|
||||
edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator)
|
||||
self.assertEqual(edit_modal.library_name, library_name)
|
||||
self.assertEqual(edit_modal.count, max_count)
|
||||
self.assertEqual(edit_modal.scored, scored)
|
||||
|
||||
@@ -109,47 +111,25 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
|
||||
"""
|
||||
Scenario: Given I have a library, a course and library content xblock in a course
|
||||
When I go to studio unit page for library content block
|
||||
And I edit set library key to none
|
||||
And I edit to select "No Library"
|
||||
Then I can see that library content block is misconfigured
|
||||
"""
|
||||
expected_text = 'A library has not yet been selected.'
|
||||
expected_action = 'Select a Library'
|
||||
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
|
||||
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[1])
|
||||
|
||||
# precondition check - the library block should be configured before we remove the library setting
|
||||
self.assertFalse(library_container.has_validation_not_configured_warning)
|
||||
|
||||
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
|
||||
edit_modal.library_key = None
|
||||
library_container.edit()
|
||||
edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator)
|
||||
edit_modal.library_name = "No Library Selected"
|
||||
library_container.save_settings()
|
||||
|
||||
self.assertTrue(library_container.has_validation_not_configured_warning)
|
||||
self.assertIn(expected_text, library_container.validation_not_configured_warning_text)
|
||||
self.assertIn(expected_action, library_container.validation_not_configured_warning_text)
|
||||
|
||||
def test_set_missing_library_shows_correct_label(self):
|
||||
"""
|
||||
Scenario: Given I have a library, a course and library content xblock in a course
|
||||
When I go to studio unit page for library content block
|
||||
And I edit set library key to non-existent library
|
||||
Then I can see that library content block is misconfigured
|
||||
"""
|
||||
nonexistent_lib_key = 'library-v1:111+111'
|
||||
expected_text = "Library is invalid, corrupt, or has been deleted."
|
||||
|
||||
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
|
||||
|
||||
# precondition check - assert library is configured before we remove it
|
||||
self.assertFalse(library_container.has_validation_error)
|
||||
|
||||
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
|
||||
edit_modal.library_key = nonexistent_lib_key
|
||||
|
||||
library_container.save_settings()
|
||||
|
||||
self.assertTrue(library_container.has_validation_error)
|
||||
self.assertIn(expected_text, library_container.validation_error_text)
|
||||
|
||||
@flaky # TODO fix this, see TE-745
|
||||
def test_out_of_date_message(self):
|
||||
"""
|
||||
@@ -162,7 +142,7 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
|
||||
Then I can see that the content no longer needs to be updated
|
||||
"""
|
||||
expected_text = "This component is out of date. The library has new content."
|
||||
library_block = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
|
||||
library_block = self._get_library_xblock_wrapper(self.unit_page.xblocks[1])
|
||||
|
||||
self.assertFalse(library_block.has_validation_warning)
|
||||
# Removed this assert until a summary message is added back to the author view (SOL-192)
|
||||
@@ -178,7 +158,7 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
|
||||
library_block.refresh_children()
|
||||
|
||||
self.unit_page.wait_for_page() # Wait for the page to reload
|
||||
library_block = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
|
||||
library_block = self._get_library_xblock_wrapper(self.unit_page.xblocks[1])
|
||||
|
||||
self.assertFalse(library_block.has_validation_message)
|
||||
# Removed this assert until a summary message is added back to the author view (SOL-192)
|
||||
@@ -206,13 +186,14 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
|
||||
|
||||
expected_text = 'There are no matching problem types in the specified libraries. Select another problem type'
|
||||
|
||||
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
|
||||
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[1])
|
||||
|
||||
# precondition check - assert library has children matching filter criteria
|
||||
self.assertFalse(library_container.has_validation_error)
|
||||
self.assertFalse(library_container.has_validation_warning)
|
||||
|
||||
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
|
||||
library_container.edit()
|
||||
edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator)
|
||||
self.assertEqual(edit_modal.capa_type, "Any Type") # precondition check
|
||||
edit_modal.capa_type = "Custom Evaluated Script"
|
||||
|
||||
@@ -221,7 +202,8 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
|
||||
self.assertTrue(library_container.has_validation_warning)
|
||||
self.assertIn(expected_text, library_container.validation_warning_text)
|
||||
|
||||
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
|
||||
library_container.edit()
|
||||
edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator)
|
||||
self.assertEqual(edit_modal.capa_type, "Custom Evaluated Script") # precondition check
|
||||
edit_modal.capa_type = "Dropdown"
|
||||
library_container.save_settings()
|
||||
@@ -240,13 +222,14 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
|
||||
expected_tpl = "The specified libraries are configured to fetch {count} problems, " \
|
||||
"but there are only {actual} matching problems."
|
||||
|
||||
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0])
|
||||
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[1])
|
||||
|
||||
# precondition check - assert block is configured fine
|
||||
self.assertFalse(library_container.has_validation_error)
|
||||
self.assertFalse(library_container.has_validation_warning)
|
||||
|
||||
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
|
||||
library_container.edit()
|
||||
edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator)
|
||||
edit_modal.count = 50
|
||||
library_container.save_settings()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user