Commit 7338cc12 by Xavier Antoviaque

Merge pull request #7148 from open-craft/library-selector

Show a select box when editing a library content block (SOL-123)
parents 41f08055 2e0beac3
...@@ -13,7 +13,6 @@ from student.roles import ( ...@@ -13,7 +13,6 @@ from student.roles import (
OrgStaffRole, OrgInstructorRole, OrgLibraryUserRole, OrgStaffRole, OrgInstructorRole, OrgLibraryUserRole,
) )
from xblock.reference.user_service import XBlockUser from xblock.reference.user_service import XBlockUser
from xmodule.library_content_module import LibraryVersionReference
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -64,7 +63,7 @@ class LibraryTestCase(ModuleStoreTestCase): ...@@ -64,7 +63,7 @@ class LibraryTestCase(ModuleStoreTestCase):
parent_location=course.location, parent_location=course.location,
user_id=self.user.id, user_id=self.user.id,
publish_item=False, publish_item=False,
source_libraries=[LibraryVersionReference(library_key)], source_library_id=unicode(library_key),
**(other_settings or {}) **(other_settings or {})
) )
...@@ -333,7 +332,7 @@ class TestLibraries(LibraryTestCase): ...@@ -333,7 +332,7 @@ class TestLibraries(LibraryTestCase):
# Now, change the block settings to have an invalid library key: # Now, change the block settings to have an invalid library key:
resp = self._update_item( resp = self._update_item(
lc_block.location, lc_block.location,
{"source_libraries": [["library-v1:NOT+FOUND", None]]}, {"source_library_id": "library-v1:NOT+FOUND"},
) )
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
lc_block = modulestore().get_item(lc_block.location) lc_block = modulestore().get_item(lc_block.location)
...@@ -376,7 +375,7 @@ class TestLibraries(LibraryTestCase): ...@@ -376,7 +375,7 @@ class TestLibraries(LibraryTestCase):
# Now, change the block settings to have an invalid library key: # Now, change the block settings to have an invalid library key:
resp = self._update_item( resp = self._update_item(
lc_block.location, lc_block.location,
{"source_libraries": [[str(library2key)]]}, {"source_library_id": str(library2key)},
) )
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
lc_block = modulestore().get_item(lc_block.location) lc_block = modulestore().get_item(lc_block.location)
...@@ -450,7 +449,7 @@ class TestLibraries(LibraryTestCase): ...@@ -450,7 +449,7 @@ class TestLibraries(LibraryTestCase):
# Now, change the block settings to have an invalid library key: # Now, change the block settings to have an invalid library key:
resp = self._update_item( resp = self._update_item(
lc_block.location, lc_block.location,
{"source_libraries": [["library-v1:NOT+FOUND", None]]}, {"source_library_id": "library-v1:NOT+FOUND"},
) )
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
......
...@@ -72,23 +72,13 @@ from django.utils.translation import ugettext as _ ...@@ -72,23 +72,13 @@ from django.utils.translation import ugettext as _
</div> </div>
</article> </article>
<aside class="content-supplementary" role="complementary"> <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: % if can_edit:
<div class="bit"> <div class="bit">
<h3 class="title-3">${_("Adding content to your library")}</h3> <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>${_("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> <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> <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> </div>
% endif % endif
<div class="bit external-help"> <div class="bit external-help">
......
...@@ -3,7 +3,7 @@ XBlock runtime services for LibraryContentModule ...@@ -3,7 +3,7 @@ XBlock runtime services for LibraryContentModule
""" """
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from opaque_keys.edx.locator import LibraryLocator 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.modulestore.exceptions import ItemNotFoundError
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
...@@ -104,38 +104,50 @@ class LibraryToolsService(object): ...@@ -104,38 +104,50 @@ class LibraryToolsService(object):
def update_children(self, dest_block, user_id, user_perms=None): def update_children(self, dest_block, user_id, user_perms=None):
""" """
This method is to be used when any of the libraries that a LibraryContentModule This method is to be used when the library that a LibraryContentModule
references have been updated. It will re-fetch all matching blocks from references has been updated. It will re-fetch all matching blocks from
the libraries, and copy them as children of dest_block. The children the libraries, and copy them as children of dest_block. The children
will be given new block_ids, but the definition ID used should be the will be given new block_ids, but the definition ID used should be the
exact same definition ID used in the library. exact same definition ID used in the library.
This method will update dest_block's 'source_libraries' field to store This method will update dest_block's 'source_library_version' field to
the version number of the libraries used, so we easily determine if store the version number of the libraries used, so we easily determine
dest_block is up to date or not. if dest_block is up to date or not.
""" """
if user_perms and not user_perms.can_write(dest_block.location.course_key): if user_perms and not user_perms.can_write(dest_block.location.course_key):
raise PermissionDenied() raise PermissionDenied()
new_libraries = [] if not dest_block.source_library_id:
dest_block.source_library_version = ""
return
source_blocks = [] source_blocks = []
for library_key, __ in dest_block.source_libraries: library_key = dest_block.source_library_key
library = self._get_library(library_key) library = self._get_library(library_key)
if library is None: if library is None:
raise ValueError("Required library not found.") raise ValueError("Requested library not found.")
if user_perms and not user_perms.can_read(library_key): if user_perms and not user_perms.can_read(library_key):
raise PermissionDenied() raise PermissionDenied()
filter_children = (dest_block.capa_type != ANY_CAPA_TYPE_VALUE) filter_children = (dest_block.capa_type != ANY_CAPA_TYPE_VALUE)
if filter_children: if filter_children:
# Apply simple filtering based on CAPA problem types: # 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)]) source_blocks.extend([key for key in library.children if self._filter_child(key, dest_block.capa_type)])
else: else:
source_blocks.extend(library.children) source_blocks.extend(library.children)
new_libraries.append(LibraryVersionReference(library_key, library.location.library_key.version_guid))
with self.store.bulk_operations(dest_block.location.course_key): 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) self.store.update_item(dest_block, user_id)
dest_block.children = self.store.copy_from_template(source_blocks, dest_block.location, 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 # ^-- copy_from_template updates the children in the DB
# but we must also set .children here to avoid overwriting the DB again # 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`. ...@@ -6,15 +6,11 @@ Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`.
""" """
from bson.objectid import ObjectId from bson.objectid import ObjectId
from mock import Mock, patch from mock import Mock, patch
from opaque_keys.edx.locator import LibraryLocator
from unittest import TestCase
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.runtime import Runtime as VanillaRuntime from xblock.runtime import Runtime as VanillaRuntime
from xmodule.library_content_module import ( from xmodule.library_content_module import ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor
LibraryVersionReference, LibraryList, ANY_CAPA_TYPE_VALUE, LibraryContentDescriptor
)
from xmodule.library_tools import LibraryToolsService from xmodule.library_tools import LibraryToolsService
from xmodule.modulestore.tests.factories import LibraryFactory, CourseFactory from xmodule.modulestore.tests.factories import LibraryFactory, CourseFactory
from xmodule.modulestore.tests.utils import MixedSplitTestCase from xmodule.modulestore.tests.utils import MixedSplitTestCase
...@@ -46,7 +42,7 @@ class LibraryContentTest(MixedSplitTestCase): ...@@ -46,7 +42,7 @@ class LibraryContentTest(MixedSplitTestCase):
"library_content", "library_content",
self.vertical, self.vertical,
max_count=1, 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): def _bind_course_module(self, module):
...@@ -128,25 +124,25 @@ class TestLibraryContentModule(LibraryContentTest): ...@@ -128,25 +124,25 @@ class TestLibraryContentModule(LibraryContentTest):
def test_validation_of_course_libraries(self): def test_validation_of_course_libraries(self):
""" """
Test that the validation method of LibraryContent blocks can validate 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: # When source_library_id 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() result = self.lc_block.validate()
self.assertFalse(result) # Validation fails due to at least one warning/message self.assertFalse(result) # Validation fails due to at least one warning/message
self.assertTrue(result.summary) self.assertTrue(result.summary)
self.assertEqual(StudioValidationMessage.NOT_CONFIGURED, result.summary.type) self.assertEqual(StudioValidationMessage.NOT_CONFIGURED, result.summary.type)
# When source_libraries references a non-existent library, we should get an error: # When source_library_id 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() result = self.lc_block.validate()
self.assertFalse(result) # Validation fails due to at least one warning/message self.assertFalse(result) # Validation fails due to at least one warning/message
self.assertTrue(result.summary) self.assertTrue(result.summary)
self.assertEqual(StudioValidationMessage.ERROR, result.summary.type) self.assertEqual(StudioValidationMessage.ERROR, result.summary.type)
self.assertIn("invalid", result.summary.text) self.assertIn("invalid", result.summary.text)
# When source_libraries is set but the block needs to be updated, the summary should say so: # When source_library_id 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() result = self.lc_block.validate()
self.assertFalse(result) # Validation fails due to at least one warning/message self.assertFalse(result) # Validation fails due to at least one warning/message
self.assertTrue(result.summary) self.assertTrue(result.summary)
...@@ -268,47 +264,6 @@ class TestLibraryContentRender(LibraryContentTest): ...@@ -268,47 +264,6 @@ class TestLibraryContentRender(LibraryContentTest):
self.assertEqual("LibraryContentAuthorView", rendered.js_init_fn) # but some js initialization should happen 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): class TestLibraryContentAnalytics(LibraryContentTest):
""" """
Test analytics features of LibraryContentModule Test analytics features of LibraryContentModule
......
...@@ -1003,6 +1003,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -1003,6 +1003,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
# 3. A generic string editor for anything else (editing JSON representation of the value). # 3. A generic string editor for anything else (editing JSON representation of the value).
editor_type = "Generic" editor_type = "Generic"
values = field.values 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: if isinstance(values, (tuple, list)) and len(values) > 0:
editor_type = "Select" editor_type = "Select"
values = [jsonify_value(field, json_choice) for json_choice in values] values = [jsonify_value(field, json_choice) for json_choice in values]
......
...@@ -4,9 +4,8 @@ Library edit page in Studio ...@@ -4,9 +4,8 @@ Library edit page in Studio
from bok_choy.javascript import js_defined, wait_for_js from bok_choy.javascript import js_defined, wait_for_js
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.select import Select from selenium.webdriver.support.select import Select
from .overview import CourseOutlineModal from .component_editor import ComponentEditorView
from .container import XBlockWrapper from .container import XBlockWrapper
from ...pages.studio.pagination import PaginatedMixin from ...pages.studio.pagination import PaginatedMixin
from ...tests.helpers import disable_animations from ...tests.helpers import disable_animations
...@@ -111,65 +110,41 @@ class LibraryPage(PageObject, PaginatedMixin): ...@@ -111,65 +110,41 @@ class LibraryPage(PageObject, PaginatedMixin):
) )
class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject): class StudioLibraryContentEditor(ComponentEditorView):
""" """
Library Content XBlock Modal edit window 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: # Labels used to identify the fields on the edit modal:
LIBRARY_LABEL = "Libraries" LIBRARY_LABEL = "Library"
COUNT_LABEL = "Count" COUNT_LABEL = "Count"
SCORED_LABEL = "Scored" SCORED_LABEL = "Scored"
PROBLEM_TYPE_LABEL = "Problem Type" 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 @property
def library_key(self): def library_name(self):
""" return self.get_selected_option_text(self.LIBRARY_LABEL)
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
@library_key.setter @library_name.setter
def library_key(self, library_key): 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) self.set_select_value(self.LIBRARY_LABEL, library_name)
if library_key_input is None: EmptyPromise(lambda: self.library_name == library_name, "library_name is updated in modal.").fulfill()
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()
@property @property
def count(self): def count(self):
""" """
Gets value of children count input 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 @count.setter
def count(self, count): def count(self, count):
""" """
Sets value of children count input 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.clear()
count_text.send_keys(count) count_text.send_keys(count)
EmptyPromise(lambda: self.count == count, "count is updated in modal.").fulfill() EmptyPromise(lambda: self.count == count, "count is updated in modal.").fulfill()
...@@ -179,7 +154,7 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject): ...@@ -179,7 +154,7 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
""" """
Gets value of scored select 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': if value == 'True':
return True return True
elif value == 'False': elif value == 'False':
...@@ -191,10 +166,7 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject): ...@@ -191,10 +166,7 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
""" """
Sets value of scored select Sets value of scored select
""" """
select_element = self.get_metadata_input(self.SCORED_LABEL) self.set_select_value(self.SCORED_LABEL, str(scored))
select_element.click()
scored_select = Select(select_element)
scored_select.select_by_value(str(scored))
EmptyPromise(lambda: self.scored == scored, "scored is updated in modal.").fulfill() EmptyPromise(lambda: self.scored == scored, "scored is updated in modal.").fulfill()
@property @property
...@@ -202,54 +174,23 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject): ...@@ -202,54 +174,23 @@ class StudioLibraryContentXBlockEditModal(CourseOutlineModal, PageObject):
""" """
Gets value of CAPA type select 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 @capa_type.setter
def capa_type(self, value): def capa_type(self, value):
""" """
Sets value of CAPA type select Sets value of CAPA type select
""" """
select_element = self.get_metadata_input(self.PROBLEM_TYPE_LABEL) self.set_select_value(self.PROBLEM_TYPE_LABEL, value)
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() 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
"""
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 Sets the select with given label (display name) to the specified value
""" """
element = self._get_metadata_element(metadata_key) elem = self.get_setting_element(label)
if element.tag_name == 'div': select = Select(elem)
# List or Dict field - return first input select.select_by_value(value)
# TODO support multiple values
inputs = self._get_list_inputs(element)
element = inputs[0] if inputs else None
return element
@js_defined('window.LibraryContentAuthorView') @js_defined('window.LibraryContentAuthorView')
......
...@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr ...@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr
from ..helpers import UniqueCourseTest from ..helpers import UniqueCourseTest
from ...pages.studio.auto_auth import AutoAuthPage from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.overview import CourseOutlinePage 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.courseware import CoursewarePage
from ...pages.lms.library import LibraryContentXBlockWrapper from ...pages.lms.library import LibraryContentXBlockWrapper
from ...pages.common.logout import LogoutPage from ...pages.common.logout import LogoutPage
...@@ -65,7 +65,7 @@ class LibraryContentTestBase(UniqueCourseTest): ...@@ -65,7 +65,7 @@ class LibraryContentTestBase(UniqueCourseTest):
) )
library_content_metadata = { library_content_metadata = {
'source_libraries': [self.library_key], 'source_library_id': unicode(self.library_key),
'mode': 'random', 'mode': 'random',
'max_count': 1, 'max_count': 1,
'has_score': False 'has_score': False
...@@ -90,12 +90,13 @@ class LibraryContentTestBase(UniqueCourseTest): ...@@ -90,12 +90,13 @@ class LibraryContentTestBase(UniqueCourseTest):
Performs library block refresh in Studio, configuring it to show {count} children Performs library block refresh in Studio, configuring it to show {count} children
""" """
unit_page = self._go_to_unit_page(True) unit_page = self._go_to_unit_page(True)
library_container_block = StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(unit_page.xblocks[0]) library_container_block = StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(unit_page.xblocks[1])
modal = StudioLibraryContentXBlockEditModal(library_container_block.edit()) library_container_block.edit()
modal.count = count editor = StudioLibraryContentEditor(self.browser, library_container_block.locator)
editor.count = count
if capa_type is not None: if capa_type is not None:
modal.capa_type = capa_type editor.capa_type = capa_type
library_container_block.save_settings() editor.save()
self._go_to_unit_page(change_login=False) self._go_to_unit_page(change_login=False)
unit_page.wait_for_page() unit_page.wait_for_page()
unit_page.publish_action.click() unit_page.publish_action.click()
......
...@@ -4,12 +4,11 @@ Acceptance tests for Library Content in LMS ...@@ -4,12 +4,11 @@ Acceptance tests for Library Content in LMS
import ddt import ddt
from flaky import flaky from flaky import flaky
import textwrap import textwrap
from unittest import skip
from .base_studio_test import StudioLibraryTest from .base_studio_test import StudioLibraryTest
from ...fixtures.course import CourseFixture from ...fixtures.course import CourseFixture
from ..helpers import UniqueCourseTest 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 ...pages.studio.overview import CourseOutlinePage
from ...fixtures.course import XBlockFixtureDesc from ...fixtures.course import XBlockFixtureDesc
...@@ -56,7 +55,7 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest): ...@@ -56,7 +55,7 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
def populate_course_fixture(self, course_fixture): def populate_course_fixture(self, course_fixture):
""" Install a course with sections/problems, tabs, updates, and handouts """ """ Install a course with sections/problems, tabs, updates, and handouts """
library_content_metadata = { library_content_metadata = {
'source_libraries': [self.library_key], 'source_library_id': unicode(self.library_key),
'mode': 'random', 'mode': 'random',
'max_count': 1, 'max_count': 1,
'has_score': False 'has_score': False
...@@ -79,29 +78,32 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest): ...@@ -79,29 +78,32 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
return StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(xblock) return StudioLibraryContainerXBlockWrapper.from_xblock_wrapper(xblock)
@ddt.data( @ddt.data(
('library-v1:111+111', 1, True), (1, True),
('library-v1:edX+L104', 2, False), (2, False),
('library-v1:OtherX+IDDQD', 3, True), (3, True),
) )
@ddt.unpack @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 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 When I go to studio unit page for library content block
And I edit library content metadata and save it And I edit library content metadata and save it
Then I can ensure that data is persisted Then I can ensure that data is persisted
""" """
library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0]) library_name = self.library_info['display_name']
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit()) library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[1])
edit_modal.library_key = library_key library_container.edit()
edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator)
edit_modal.library_name = library_name
edit_modal.count = max_count edit_modal.count = max_count
edit_modal.scored = scored edit_modal.scored = scored
library_container.save_settings() # saving settings library_container.save_settings() # saving settings
# open edit window again to verify changes are persistent # open edit window again to verify changes are persistent
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit()) library_container.edit()
self.assertEqual(edit_modal.library_key, library_key) 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.count, max_count)
self.assertEqual(edit_modal.scored, scored) self.assertEqual(edit_modal.scored, scored)
...@@ -109,47 +111,25 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest): ...@@ -109,47 +111,25 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
""" """
Scenario: Given I have a library, a course and library content xblock in a course 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 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 Then I can see that library content block is misconfigured
""" """
expected_text = 'A library has not yet been selected.' expected_text = 'A library has not yet been selected.'
expected_action = 'Select a Library' 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 # precondition check - the library block should be configured before we remove the library setting
self.assertFalse(library_container.has_validation_not_configured_warning) self.assertFalse(library_container.has_validation_not_configured_warning)
edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit()) library_container.edit()
edit_modal.library_key = None edit_modal = StudioLibraryContentEditor(self.browser, library_container.locator)
edit_modal.library_name = "No Library Selected"
library_container.save_settings() library_container.save_settings()
self.assertTrue(library_container.has_validation_not_configured_warning) self.assertTrue(library_container.has_validation_not_configured_warning)
self.assertIn(expected_text, library_container.validation_not_configured_warning_text) self.assertIn(expected_text, library_container.validation_not_configured_warning_text)
self.assertIn(expected_action, 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 @flaky # TODO fix this, see TE-745
def test_out_of_date_message(self): def test_out_of_date_message(self):
""" """
...@@ -162,7 +142,7 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest): ...@@ -162,7 +142,7 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
Then I can see that the content no longer needs to be updated 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." 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) self.assertFalse(library_block.has_validation_warning)
# Removed this assert until a summary message is added back to the author view (SOL-192) # Removed this assert until a summary message is added back to the author view (SOL-192)
...@@ -178,7 +158,7 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest): ...@@ -178,7 +158,7 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
library_block.refresh_children() library_block.refresh_children()
self.unit_page.wait_for_page() # Wait for the page to reload 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) self.assertFalse(library_block.has_validation_message)
# Removed this assert until a summary message is added back to the author view (SOL-192) # Removed this assert until a summary message is added back to the author view (SOL-192)
...@@ -206,13 +186,14 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest): ...@@ -206,13 +186,14 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
expected_text = 'There are no matching problem types in the specified libraries. Select another problem type' 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 # precondition check - assert library has children matching filter criteria
self.assertFalse(library_container.has_validation_error) self.assertFalse(library_container.has_validation_error)
self.assertFalse(library_container.has_validation_warning) 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 self.assertEqual(edit_modal.capa_type, "Any Type") # precondition check
edit_modal.capa_type = "Custom Evaluated Script" edit_modal.capa_type = "Custom Evaluated Script"
...@@ -221,7 +202,8 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest): ...@@ -221,7 +202,8 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
self.assertTrue(library_container.has_validation_warning) self.assertTrue(library_container.has_validation_warning)
self.assertIn(expected_text, library_container.validation_warning_text) 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 self.assertEqual(edit_modal.capa_type, "Custom Evaluated Script") # precondition check
edit_modal.capa_type = "Dropdown" edit_modal.capa_type = "Dropdown"
library_container.save_settings() library_container.save_settings()
...@@ -237,16 +219,17 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest): ...@@ -237,16 +219,17 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
And I set Problem Type selector so "Any" And I set Problem Type selector so "Any"
Then I can see that "No matching content" warning is shown Then I can see that "No matching content" warning is shown
""" """
expected_tpl = "The specified libraries are configured to fetch {count} problems, " \ expected_tpl = "The specified library is configured to fetch {count} problems, " \
"but there are only {actual} matching 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 # precondition check - assert block is configured fine
self.assertFalse(library_container.has_validation_error) self.assertFalse(library_container.has_validation_error)
self.assertFalse(library_container.has_validation_warning) 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 edit_modal.count = 50
library_container.save_settings() library_container.save_settings()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment