diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index a0b74372e255..0ff086ea6214 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -58,6 +58,8 @@
CorrectMap = correctmap.CorrectMap # pylint: disable=invalid-name
CORRECTMAP_PY = None
+# Make '_' a no-op so we can scrape strings
+_ = lambda text: text
#-----------------------------------------------------------------------------
# Exceptions
@@ -439,6 +441,7 @@ class JavascriptResponse(LoncapaResponse):
Javascript using Node.js.
"""
+ human_name = _('Javascript Input')
tags = ['javascriptresponse']
max_inputfields = 1
allowed_inputfields = ['javascriptinput']
@@ -684,6 +687,7 @@ class ChoiceResponse(LoncapaResponse):
"""
+ human_name = _('Checkboxes')
tags = ['choiceresponse']
max_inputfields = 1
allowed_inputfields = ['checkboxgroup', 'radiogroup']
@@ -754,6 +758,7 @@ class MultipleChoiceResponse(LoncapaResponse):
"""
# TODO: handle direction and randomize
+ human_name = _('Multiple Choice')
tags = ['multiplechoiceresponse']
max_inputfields = 1
allowed_inputfields = ['choicegroup']
@@ -1042,6 +1047,7 @@ def sample_from_answer_pool(self, choices, rng, num_pool):
@registry.register
class TrueFalseResponse(MultipleChoiceResponse):
+ human_name = _('True/False Choice')
tags = ['truefalseresponse']
def mc_setup_response(self):
@@ -1073,6 +1079,7 @@ class OptionResponse(LoncapaResponse):
TODO: handle direction and randomize
"""
+ human_name = _('Dropdown')
tags = ['optionresponse']
hint_tag = 'optionhint'
allowed_inputfields = ['optioninput']
@@ -1108,6 +1115,7 @@ class NumericalResponse(LoncapaResponse):
to a number (e.g. `4+5/2^2`), and accepts with a tolerance.
"""
+ human_name = _('Numerical Input')
tags = ['numericalresponse']
hint_tag = 'numericalhint'
allowed_inputfields = ['textline', 'formulaequationinput']
@@ -1308,6 +1316,7 @@ class StringResponse(LoncapaResponse):
"""
+ human_name = _('Text Input')
tags = ['stringresponse']
hint_tag = 'stringhint'
allowed_inputfields = ['textline']
@@ -1426,6 +1435,7 @@ class CustomResponse(LoncapaResponse):
or in a
"""
+ human_name = _('Custom Evaluated Script')
tags = ['customresponse']
allowed_inputfields = ['textline', 'textbox', 'crystallography',
@@ -1797,6 +1807,7 @@ class SymbolicResponse(CustomResponse):
Symbolic math response checking, using symmath library.
"""
+ human_name = _('Symbolic Math Input')
tags = ['symbolicresponse']
max_inputfields = 1
@@ -1865,6 +1876,7 @@ class CodeResponse(LoncapaResponse):
"""
+ human_name = _('Code Input')
tags = ['coderesponse']
allowed_inputfields = ['textbox', 'filesubmission', 'matlabinput']
max_inputfields = 1
@@ -2142,6 +2154,7 @@ class ExternalResponse(LoncapaResponse):
"""
+ human_name = _('External Grader')
tags = ['externalresponse']
allowed_inputfields = ['textline', 'textbox']
awdmap = {
@@ -2299,6 +2312,7 @@ class FormulaResponse(LoncapaResponse):
Checking of symbolic math response using numerical sampling.
"""
+ human_name = _('Math Expression Input')
tags = ['formularesponse']
hint_tag = 'formulahint'
allowed_inputfields = ['textline', 'formulaequationinput']
@@ -2511,6 +2525,7 @@ class SchematicResponse(LoncapaResponse):
"""
Circuit schematic response type.
"""
+ human_name = _('Circuit Schematic Builder')
tags = ['schematicresponse']
allowed_inputfields = ['schematic']
@@ -2589,6 +2604,7 @@ class ImageResponse(LoncapaResponse):
True, if click is inside any region or rectangle. Otherwise False.
"""
+ human_name = _('Image Mapped Input')
tags = ['imageresponse']
allowed_inputfields = ['imageinput']
@@ -2707,6 +2723,7 @@ class AnnotationResponse(LoncapaResponse):
The response contains both a comment (student commentary) and an option (student tag).
Only the tag is currently graded. Answers may be incorrect, partially correct, or correct.
"""
+ human_name = _('Annotation Input')
tags = ['annotationresponse']
allowed_inputfields = ['annotationinput']
max_inputfields = 1
@@ -2831,6 +2848,7 @@ class ChoiceTextResponse(LoncapaResponse):
ChoiceResponse.
"""
+ human_name = _('Checkboxes With Text Input')
tags = ['choicetextresponse']
max_inputfields = 1
allowed_inputfields = ['choicetextgroup',
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 776dc36e3fb9..47583d97065a 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -2,10 +2,12 @@
import json
import logging
import sys
+from lxml import etree
from pkg_resources import resource_string
from .capa_base import CapaMixin, CapaFields, ComplexEncoder
+from capa import responsetypes
from .progress import Progress
from xmodule.x_module import XModule, module_attr
from xmodule.raw_module import RawDescriptor
@@ -172,6 +174,13 @@ def non_editable_metadata_fields(self):
])
return non_editable_fields
+ @property
+ def problem_types(self):
+ """ Low-level problem type introspection for content libraries filtering by problem type """
+ tree = etree.XML(self.data)
+ registered_tags = responsetypes.registry.registered_tags()
+ return set([node.tag for node in tree.iter() if node.tag in registered_tags])
+
# Proxy to CapaModule for access to any of its attributes
answer_available = module_attr('answer_available')
check_button_name = module_attr('check_button_name')
diff --git a/common/lib/xmodule/xmodule/library_content_module.py b/common/lib/xmodule/xmodule/library_content_module.py
index 8571540f7589..a3d632292961 100644
--- a/common/lib/xmodule/xmodule/library_content_module.py
+++ b/common/lib/xmodule/xmodule/library_content_module.py
@@ -5,6 +5,8 @@
from bson.objectid import ObjectId, InvalidId
from collections import namedtuple
from copy import copy
+from capa.responsetypes import registry
+
from .mako_module import MakoModuleDescriptor
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locator import LibraryLocator
@@ -19,15 +21,38 @@
from .xml_module import XmlDescriptor
from pkg_resources import resource_string
+
# Make '_' a no-op so we can scrape strings
_ = 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.
+ """
+ return getattr(problem_class, 'human_name', problem_class.__name__)
+
+
+def _get_capa_types():
+ """
+ Gets capa types tags and labels
+ """
+ capa_types = {tag: _get_human_name(registry.get_class_for_tag(tag)) for tag in registry.registered_tags()}
+
+ return [{'value': ANY_CAPA_TYPE_VALUE, 'display_name': _('Any Type')}] + sorted([
+ {'value': capa_type, 'display_name': caption}
+ for capa_type, caption in capa_types.items()
+ ], key=lambda item: item.get('display_name'))
+
+
class LibraryVersionReference(namedtuple("LibraryVersionReference", "library_id version")):
"""
A reference to a specific library, with an optional version.
@@ -146,6 +171,13 @@ class LibraryContentFields(object):
default=1,
scope=Scope.settings,
)
+ capa_type = String(
+ display_name=_("Problem Type"),
+ help=_('Choose a problem type to fetch from the library. If "Any Type" is selected no filtering is applied.'),
+ default=ANY_CAPA_TYPE_VALUE,
+ values=_get_capa_types(),
+ scope=Scope.settings,
+ )
filters = String(default="") # TBD
has_score = Boolean(
display_name=_("Scored"),
@@ -311,6 +343,39 @@ def refresh_children(self, request, suffix, update_db=True): # pylint: disable=
lib_tools.update_children(self, user_id, update_db)
return Response()
+ def _validate_library_version(self, validation, lib_tools, version, library_key):
+ """
+ Validates library version
+ """
+ latest_version = lib_tools.get_library_version(library_key)
+ if latest_version is not None:
+ if version is None or version != latest_version:
+ validation.set_summary(
+ StudioValidationMessage(
+ StudioValidationMessage.WARNING,
+ _(u'This component is out of date. The library has new content.'),
+ action_class='library-update-btn', # TODO: change this to action_runtime_event='...' once the unit page supports that feature.
+ action_label=_(u"↻ Update now")
+ )
+ )
+ return False
+ else:
+ validation.set_summary(
+ StudioValidationMessage(
+ StudioValidationMessage.ERROR,
+ _(u'Library is invalid, corrupt, or has been deleted.'),
+ action_class='edit-button',
+ action_label=_(u"Edit Library List")
+ )
+ )
+ return False
+ return True
+
+ def _set_validation_error_if_empty(self, validation, summary):
+ """ Helper method to only set validation summary if it's empty """
+ if validation.empty:
+ validation.set_summary(summary)
+
def validate(self):
"""
Validates the state of this Library Content Module Instance. This
@@ -331,30 +396,41 @@ def validate(self):
)
return validation
lib_tools = self.runtime.service(self, 'library_tools')
+ matching_children_count = 0
for library_key, version in self.source_libraries:
- latest_version = lib_tools.get_library_version(library_key)
- if latest_version is not None:
- if version is None or version != latest_version:
- validation.set_summary(
- StudioValidationMessage(
- StudioValidationMessage.WARNING,
- _(u'This component is out of date. The library has new content.'),
- action_class='library-update-btn', # TODO: change this to action_runtime_event='...' once the unit page supports that feature.
- action_label=_(u"↻ Update now")
- )
- )
- break
- else:
- validation.set_summary(
- StudioValidationMessage(
- StudioValidationMessage.ERROR,
- _(u'Library is invalid, corrupt, or has been deleted.'),
- action_class='edit-button',
- action_label=_(u"Edit Library List")
- )
- )
+ if not self._validate_library_version(validation, lib_tools, version, library_key):
break
+ library = lib_tools.get_library(library_key)
+ children_matching_filter = lib_tools.get_filtered_children(library, self.capa_type)
+ # get_filtered_children returns generator, so can't use len.
+ # And we don't actually need those children, so no point of constructing a list
+ matching_children_count += sum(1 for child in children_matching_filter)
+
+ if matching_children_count == 0:
+ self._set_validation_error_if_empty(
+ validation,
+ StudioValidationMessage(
+ StudioValidationMessage.WARNING,
+ _(u'There are no matching problem types in the specified libraries.'),
+ action_class='edit-button',
+ action_label=_(u"Select another problem type")
+ )
+ )
+
+ if matching_children_count < self.max_count:
+ self._set_validation_error_if_empty(
+ validation,
+ StudioValidationMessage(
+ StudioValidationMessage.WARNING,
+ _(u'The specified libraries are configured to fetch {count} problems, '
+ u'but there are only {actual} matching problems.')
+ .format(actual=matching_children_count, count=self.max_count),
+ action_class='edit-button',
+ action_label=_(u"Edit configuration")
+ )
+ )
+
return validation
def editor_saved(self, user, old_metadata, old_content):
@@ -362,7 +438,8 @@ def editor_saved(self, user, old_metadata, old_content):
If source_libraries 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):
+ if set(old_source_libraries) != set(self.source_libraries) or \
+ old_metadata.get('capa_type', ANY_CAPA_TYPE_VALUE) != self.capa_type:
try:
self.refresh_children(None, None, update_db=False) # update_db=False since update_item() is about to be called anyways
except ValueError:
diff --git a/common/lib/xmodule/xmodule/library_tools.py b/common/lib/xmodule/xmodule/library_tools.py
index 0ff50d81a911..cb250586d395 100644
--- a/common/lib/xmodule/xmodule/library_tools.py
+++ b/common/lib/xmodule/xmodule/library_tools.py
@@ -4,8 +4,9 @@
import hashlib
from opaque_keys.edx.locator import LibraryLocator
from xblock.fields import Scope
-from xmodule.library_content_module import LibraryVersionReference
+from xmodule.library_content_module import LibraryVersionReference, ANY_CAPA_TYPE_VALUE
from xmodule.modulestore.exceptions import ItemNotFoundError
+from xmodule.capa_module import CapaDescriptor
class LibraryToolsService(object):
@@ -16,7 +17,7 @@ class LibraryToolsService(object):
def __init__(self, modulestore):
self.store = modulestore
- def _get_library(self, library_key):
+ def get_library(self, library_key):
"""
Given a library key like "library-v1:ProblemX+PR0B", return the
'library' XBlock with meta-information about the library.
@@ -37,7 +38,7 @@ def get_library_version(self, lib_key):
Get the version (an ObjectID) of the given library.
Returns None if the library does not exist.
"""
- library = self._get_library(lib_key)
+ library = self.get_library(lib_key)
if library:
# We need to know the library's version so ensure it's set in library.location.library_key.version_guid
assert library.location.library_key.version_guid is not None
@@ -49,11 +50,38 @@ def get_library_display_name(self, lib_key):
Get the display_name of the given library.
Returns None if the library does not exist.
"""
- library = self._get_library(lib_key)
+ library = self.get_library(lib_key)
if library:
return library.display_name
return None
+ def _filter_child(self, capa_type, child_descriptor):
+ """
+ Filters children by CAPA problem type, if configured
+ """
+ if capa_type == ANY_CAPA_TYPE_VALUE:
+ return True
+
+ if not isinstance(child_descriptor, CapaDescriptor):
+ return False
+
+ return capa_type in child_descriptor.problem_types
+
+ def get_filtered_children(self, from_block, capa_type=ANY_CAPA_TYPE_VALUE):
+ """
+ Filters children of `from_block` that satisfy filter criteria
+ Returns generator containing (child_key, child) for all children matching filter criteria
+ """
+ children = (
+ (child_key, self.store.get_item(child_key, depth=9))
+ for child_key in from_block.children
+ )
+ return (
+ (child_key, child)
+ for child_key, child in children
+ if self._filter_child(capa_type, child)
+ )
+
def update_children(self, dest_block, user_id, update_db=True):
"""
This method is to be used when any of the libraries that a LibraryContentModule
@@ -83,7 +111,7 @@ def update_children(self, dest_block, user_id, update_db=True):
# First, load and validate the source_libraries:
libraries = []
for library_key, old_version in dest_block.source_libraries: # pylint: disable=unused-variable
- library = self._get_library(library_key)
+ library = self.get_library(library_key)
if library is None:
raise ValueError("Required library not found.")
libraries.append((library_key, library))
@@ -96,13 +124,14 @@ def update_children(self, dest_block, user_id, update_db=True):
new_libraries = []
for library_key, library in libraries:
- def copy_children_recursively(from_block):
+ def copy_children_recursively(from_block, filter_problem_type=False):
"""
Internal method to copy blocks from the library recursively
"""
new_children = []
- for child_key in from_block.children:
- child = self.store.get_item(child_key, depth=9)
+ target_capa_type = dest_block.capa_type if filter_problem_type else ANY_CAPA_TYPE_VALUE
+ filtered_children = self.get_filtered_children(from_block, target_capa_type)
+ for child_key, child in filtered_children:
# We compute a block_id for each matching child block found in the library.
# block_ids are unique within any branch, but are not unique per-course or globally.
# We need our block_ids to be consistent when content in the library is updated, so
@@ -130,7 +159,7 @@ def copy_children_recursively(from_block):
)
new_children.append(new_child_info.location)
return new_children
- root_children.extend(copy_children_recursively(from_block=library))
+ root_children.extend(copy_children_recursively(from_block=library, filter_problem_type=True))
new_libraries.append(LibraryVersionReference(library_key, library.location.library_key.version_guid))
dest_block.source_libraries = new_libraries
dest_block.children = root_children
diff --git a/common/test/acceptance/pages/lms/library.py b/common/test/acceptance/pages/lms/library.py
index 8655fae79f55..6978b5fa0b1e 100644
--- a/common/test/acceptance/pages/lms/library.py
+++ b/common/test/acceptance/pages/lms/library.py
@@ -16,6 +16,9 @@ def __init__(self, browser, locator):
self.locator = locator
def is_browser_on_page(self):
+ """
+ Checks if page is opened
+ """
return self.q(css='{}[data-id="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
def _bounded_selector(self, selector):
@@ -35,3 +38,11 @@ def children_contents(self):
"""
child_blocks = self.q(css=self._bounded_selector("div[data-id]"))
return frozenset(child.text for child in child_blocks)
+
+ @property
+ def children_headers(self):
+ """
+ Gets headers of all child XBlocks as list of strings
+ """
+ child_blocks_headers = self.q(css=self._bounded_selector("div[data-id] h2.problem-header"))
+ return frozenset(child.text for child in child_blocks_headers)
diff --git a/common/test/acceptance/pages/studio/library.py b/common/test/acceptance/pages/studio/library.py
index ea7f2299f961..71df4c4ad18d 100644
--- a/common/test/acceptance/pages/studio/library.py
+++ b/common/test/acceptance/pages/studio/library.py
@@ -122,6 +122,7 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
LIBRARY_LABEL = "Libraries"
COUNT_LABEL = "Count"
SCORED_LABEL = "Scored"
+ PROBLEM_TYPE_LABEL = "Problem Type"
def is_browser_on_page(self):
"""
@@ -196,6 +197,24 @@ def scored(self, scored):
scored_select.select_by_value(str(scored))
EmptyPromise(lambda: self.scored == scored, "scored is updated in modal.").fulfill()
+ @property
+ def capa_type(self):
+ """
+ Gets value of CAPA type select
+ """
+ return self.get_metadata_input(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)
+ EmptyPromise(lambda: self.capa_type == value, "problem type is updated in modal.").fulfill()
+
def _add_library_key(self):
"""
Adds library key input
diff --git a/common/test/acceptance/tests/lms/test_library.py b/common/test/acceptance/tests/lms/test_library.py
index f83e6b94e9d5..d152f4338687 100644
--- a/common/test/acceptance/tests/lms/test_library.py
+++ b/common/test/acceptance/tests/lms/test_library.py
@@ -19,22 +19,25 @@
UNIT_NAME = 'Test Unit'
-@ddt.ddt
-class LibraryContentTest(UniqueCourseTest):
- """
- Test courseware.
- """
+class LibraryContentTestBase(UniqueCourseTest):
+ """ Base class for library content block tests """
USERNAME = "STUDENT_TESTER"
EMAIL = "student101@example.com"
STAFF_USERNAME = "STAFF_TESTER"
STAFF_EMAIL = "staff101@example.com"
+ def populate_library_fixture(self, library_fixture):
+ """
+ To be overwritten by subclassed tests. Used to install a library to
+ run tests on.
+ """
+
def setUp(self):
"""
Set up library, course and library content XBlock
"""
- super(LibraryContentTest, self).setUp()
+ super(LibraryContentTestBase, self).setUp()
self.courseware_page = CoursewarePage(self.browser, self.course_id)
@@ -46,11 +49,7 @@ def setUp(self):
)
self.library_fixture = LibraryFixture('test_org', self.unique_id, 'Test Library {}'.format(self.unique_id))
- self.library_fixture.add_children(
- XBlockFixtureDesc("html", "Html1", data='html1'),
- XBlockFixtureDesc("html", "Html2", data='html2'),
- XBlockFixtureDesc("html", "Html3", data='html3'),
- )
+ self.populate_library_fixture(self.library_fixture)
self.library_fixture.install()
self.library_info = self.library_fixture.library_info
@@ -83,7 +82,7 @@ def setUp(self):
self.course_fixture.install()
- def _refresh_library_content_children(self, count=1):
+ def _change_library_content_settings(self, count=1, capa_type=None):
"""
Performs library block refresh in Studio, configuring it to show {count} children
"""
@@ -91,6 +90,8 @@ def _refresh_library_content_children(self, count=1):
library_container_block = StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(unit_page.xblocks[0])
modal = StudioLibraryContentXBlockEditModal(library_container_block.edit())
modal.count = count
+ if capa_type is not None:
+ modal.capa_type = capa_type
library_container_block.save_settings()
self._go_to_unit_page(change_login=False)
unit_page.wait_for_page()
@@ -121,9 +122,13 @@ def _goto_library_block_page(self, block_id=None):
Open library page in LMS
"""
self.courseware_page.visit()
+ paragraphs = self.courseware_page.q(css='.course-content p')
+ if paragraphs and "You were most recently in" in paragraphs.text[0]:
+ paragraphs[0].find_element_by_tag_name('a').click()
block_id = block_id if block_id is not None else self.lib_block.locator
#pylint: disable=attribute-defined-outside-init
self.library_content_page = LibraryContentXBlockWrapper(self.browser, block_id)
+ self.library_content_page.wait_for_page()
def _auto_auth(self, username, email, staff):
"""
@@ -132,6 +137,22 @@ def _auto_auth(self, username, email, staff):
AutoAuthPage(self.browser, username=username, email=email,
course_id=self.course_id, staff=staff).visit()
+
+@ddt.ddt
+class LibraryContentTest(LibraryContentTestBase):
+ """
+ Test courseware.
+ """
+ def populate_library_fixture(self, library_fixture):
+ """
+ Populates library fixture with XBlock Fixtures
+ """
+ library_fixture.add_children(
+ XBlockFixtureDesc("html", "Html1", data='html1'),
+ XBlockFixtureDesc("html", "Html2", data='html2'),
+ XBlockFixtureDesc("html", "Html3", data='html3'),
+ )
+
@ddt.data(1, 2, 3)
def test_shows_random_xblocks_from_configured(self, count):
"""
@@ -143,7 +164,7 @@ def test_shows_random_xblocks_from_configured(self, count):
When I go to LMS courseware page for library content xblock as student
Then I can see {count} random xblocks from the library
"""
- self._refresh_library_content_children(count=count)
+ self._change_library_content_settings(count=count)
self._auto_auth(self.USERNAME, self.EMAIL, False)
self._goto_library_block_page()
children_contents = self.library_content_page.children_contents
@@ -160,9 +181,120 @@ def test_shows_all_if_max_set_to_greater_value(self):
When I go to LMS courseware page for library content xblock as student
Then I can see all xblocks from the library
"""
- self._refresh_library_content_children(count=10)
+ self._change_library_content_settings(count=10)
self._auto_auth(self.USERNAME, self.EMAIL, False)
self._goto_library_block_page()
children_contents = self.library_content_page.children_contents
self.assertEqual(len(children_contents), 3)
self.assertEqual(children_contents, self.library_xblocks_texts)
+
+
+@ddt.ddt
+class StudioLibraryContainerCapaFilterTest(LibraryContentTestBase):
+ """
+ Test Library Content block in LMS
+ """
+ def _get_problem_choice_group_text(self, name, items):
+ """ Generates Choice Group CAPA problem XML """
+ items_text = "\n".join([
+ "{item}".format(correct=correct, item=item)
+ for item, correct in items
+ ])
+
+ return """
+ {name}
+
+ {items}
+
+""".format(name=name, items=items_text)
+
+ def _get_problem_select_text(self, name, items, correct):
+ """ Generates Select Option CAPA problem XML """
+ items_text = ",".join(["'{0}'".format(item) for item in items])
+
+ return """
+{name}
+
+
+
+""".format(name=name, options=items_text, correct=correct)
+
+ def populate_library_fixture(self, library_fixture):
+ """
+ Populates library fixture with XBlock Fixtures
+ """
+ library_fixture.add_children(
+ XBlockFixtureDesc(
+ "problem", "Problem Choice Group 1",
+ data=self._get_problem_choice_group_text("Problem Choice Group 1 Text", [("1", False), ('2', True)])
+ ),
+ XBlockFixtureDesc(
+ "problem", "Problem Choice Group 2",
+ data=self._get_problem_choice_group_text("Problem Choice Group 2 Text", [("Q", True), ('W', False)])
+ ),
+ XBlockFixtureDesc(
+ "problem", "Problem Select 1",
+ data=self._get_problem_select_text("Problem Select 1 Text", ["Option 1", "Option 2"], "Option 1")
+ ),
+ XBlockFixtureDesc(
+ "problem", "Problem Select 2",
+ data=self._get_problem_select_text("Problem Select 2 Text", ["Option 3", "Option 4"], "Option 4")
+ ),
+ )
+
+ @property
+ def _problem_headers(self):
+ """ Expected XBLock headers according to populate_library_fixture """
+ return frozenset(child.display_name.upper() for child in self.library_fixture.children)
+
+ def _set_library_content_settings(self, count=1, capa_type="Any Type"):
+ """
+ Sets library content XBlock parameters, saves, publishes unit, goes to LMS unit page and
+ gets children XBlock headers to assert against them
+ """
+ self._change_library_content_settings(count=count, capa_type=capa_type)
+ self._auto_auth(self.USERNAME, self.EMAIL, False)
+ self._goto_library_block_page()
+ return self.library_content_page.children_headers
+
+ def test_problem_type_selector(self):
+ """
+ Scenario: Ensure setting "Any Type" for Problem Type does not filter out Problems
+ Given I have a library with two "Select Option" and two "Choice Group" problems, and a course containing
+ LibraryContent XBlock configured to draw XBlocks from that library
+ When I set library content xblock Problem Type to "Any Type" and Count to 3 and publish unit
+ When I go to LMS courseware page for library content xblock as student
+ Then I can see 3 xblocks from the library of any type
+ When I set library content xblock Problem Type to "Choice Group" and Count to 1 and publish unit
+ When I go to LMS courseware page for library content xblock as student
+ Then I can see 1 xblock from the library of "Choice Group" type
+ When I set library content xblock Problem Type to "Select Option" and Count to 2 and publish unit
+ When I go to LMS courseware page for library content xblock as student
+ Then I can see 2 xblock from the library of "Select Option" type
+ When I set library content xblock Problem Type to "Matlab" and Count to 2 and publish unit
+ When I go to LMS courseware page for library content xblock as student
+ Then I can see 0 xblocks from the library
+ """
+ children_headers = self._set_library_content_settings(count=3, capa_type="Any Type")
+ self.assertEqual(len(children_headers), 3)
+ self.assertLessEqual(children_headers, self._problem_headers)
+
+ # Choice group test
+ children_headers = self._set_library_content_settings(count=1, capa_type="Multiple Choice")
+ self.assertEqual(len(children_headers), 1)
+ self.assertLessEqual(
+ children_headers,
+ set([header.upper() for header in ["Problem Choice Group 1", "Problem Choice Group 2"]])
+ )
+
+ # Choice group test
+ children_headers = self._set_library_content_settings(count=2, capa_type="Dropdown")
+ self.assertEqual(len(children_headers), 2)
+ self.assertLessEqual(
+ children_headers,
+ set([header.upper() for header in ["Problem Select 1", "Problem Select 2"]])
+ )
+
+ # Missing problem type test
+ children_headers = self._set_library_content_settings(count=2, capa_type="Custom Evaluated Script")
+ self.assertEqual(children_headers, set())
diff --git a/common/test/acceptance/tests/studio/test_studio_library_container.py b/common/test/acceptance/tests/studio/test_studio_library_container.py
index d7a592c79fce..1521b54cbc00 100644
--- a/common/test/acceptance/tests/studio/test_studio_library_container.py
+++ b/common/test/acceptance/tests/studio/test_studio_library_container.py
@@ -33,6 +33,14 @@ def populate_library_fixture(self, library_fixture):
XBlockFixtureDesc("html", "Html1"),
XBlockFixtureDesc("html", "Html2"),
XBlockFixtureDesc("html", "Html3"),
+
+ XBlockFixtureDesc(
+ "problem", "Dropdown",
+ data="""
+
+ Dropdown
+
+""")
)
def populate_course_fixture(self, course_fixture):
@@ -162,3 +170,64 @@ def test_out_of_date_message(self):
self.assertFalse(library_block.has_validation_message)
#self.assertIn("4 matching components", library_block.author_content) # Removed this assert until a summary message is added back to the author view (SOL-192)
+
+ def test_no_content_message(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 set Problem Type selector so that no libraries have matching content
+ Then I can see that "No matching content" warning is shown
+ When I set Problem Type selector so that there are matching content
+ Then I can see that warning messages are not shown
+ """
+ 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])
+
+ # 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())
+ self.assertEqual(edit_modal.capa_type, "Any Type") # precondition check
+ edit_modal.capa_type = "Custom Evaluated Script"
+
+ library_container.save_settings()
+
+ self.assertTrue(library_container.has_validation_warning)
+ self.assertIn(expected_text, library_container.validation_warning_text)
+
+ edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit())
+ self.assertEqual(edit_modal.capa_type, "Custom Evaluated Script") # precondition check
+ edit_modal.capa_type = "Dropdown"
+ library_container.save_settings()
+
+ # Library should contain single Dropdown problem, so now there should be no errors again
+ self.assertFalse(library_container.has_validation_error)
+ self.assertFalse(library_container.has_validation_warning)
+
+ def test_not_enough_children_blocks(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 set Problem Type selector so "Any"
+ Then I can see that "No matching content" warning is shown
+ """
+ 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])
+
+ # 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())
+ edit_modal.count = 50
+ library_container.save_settings()
+
+ self.assertTrue(library_container.has_validation_warning)
+ self.assertIn(
+ expected_tpl.format(count=50, actual=len(self.library_fixture.children)),
+ library_container.validation_warning_text
+ )