Merge remote-tracking branch 'edx/master' into opaque-keys
Conflicts: cms/djangoapps/contentstore/tests/test_contentstore.py cms/djangoapps/contentstore/views/component.py cms/djangoapps/contentstore/views/item.py cms/djangoapps/contentstore/views/preview.py cms/djangoapps/contentstore/views/tests/test_container.py cms/static/js/spec/views/unit_spec.js cms/static/js/utils/module.js cms/templates/container.html cms/templates/studio_vertical_wrapper.html cms/templates/studio_xblock_wrapper.html common/djangoapps/student/views.py lms/templates/notes.html lms/templates/textannotation.html lms/templates/videoannotation.html
This commit is contained in:
@@ -204,8 +204,9 @@ def add_subsection(name='Subsection One'):
|
||||
|
||||
def set_date_and_time(date_css, desired_date, time_css, desired_time, key=None):
|
||||
set_element_value(date_css, desired_date, key)
|
||||
set_element_value(time_css, desired_time, key)
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
set_element_value(time_css, desired_time, key)
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
|
||||
@@ -38,14 +38,16 @@ Feature: CMS.Create Subsection
|
||||
Then I see the subsection release date is 12/25/2011 03:00
|
||||
And I see the subsection due date is 01/02/2012 04:00
|
||||
|
||||
# Disabling due to failure on master. JZ 05/14/2014 TODO: fix
|
||||
# Scenario: Set release and due dates of subsection on enter
|
||||
# Given I have opened a new subsection in Studio
|
||||
# And I set the subsection release date on enter to 04/04/2014 03:00
|
||||
# And I set the subsection due date on enter to 04/04/2014 04:00
|
||||
# And I reload the page
|
||||
# Then I see the subsection release date is 04/04/2014 03:00
|
||||
# And I see the subsection due date is 04/04/2014 04:00
|
||||
@skip_safari
|
||||
Scenario: Set release and due dates of subsection on enter
|
||||
Given I have opened a new subsection in Studio
|
||||
And I set the subsection release date on enter to 04/04/2014 03:00
|
||||
And I set the subsection due date on enter to 04/04/2014 04:00
|
||||
Then I see the subsection release date is 04/04/2014 03:00
|
||||
And I see the subsection due date is 04/04/2014 04:00
|
||||
And I reload the page
|
||||
Then I see the subsection release date is 04/04/2014 03:00
|
||||
And I see the subsection due date is 04/04/2014 04:00
|
||||
|
||||
Scenario: Delete a subsection
|
||||
Given I have opened a new course section in Studio
|
||||
@@ -56,16 +58,18 @@ Feature: CMS.Create Subsection
|
||||
And I confirm the prompt
|
||||
Then the subsection does not exist
|
||||
|
||||
# Disabling due to failure on master. JZ 05/14/2014 TODO: fix
|
||||
# Scenario: Sync to Section
|
||||
# Given I have opened a new course section in Studio
|
||||
# And I click the Edit link for the release date
|
||||
# And I set the section release date to 01/02/2103
|
||||
# And I have added a new subsection
|
||||
# And I click on the subsection
|
||||
# And I set the subsection release date to 01/20/2103
|
||||
# And I reload the page
|
||||
# And I click the link to sync release date to section
|
||||
# And I wait for "1" second
|
||||
# And I reload the page
|
||||
# Then I see the subsection release date is 01/02/2103
|
||||
@skip_safari
|
||||
Scenario: Sync to Section
|
||||
Given I have opened a new course section in Studio
|
||||
And I click the Edit link for the release date
|
||||
And I set the section release date to 01/02/2103
|
||||
And I have added a new subsection
|
||||
And I click on the subsection
|
||||
And I set the subsection release date to 06/20/2104
|
||||
Then I see the subsection release date is 06/20/2104
|
||||
And I reload the page
|
||||
Then I see the subsection release date is 06/20/2104
|
||||
And I click the link to sync release date to section
|
||||
And I wait for "1" second
|
||||
And I reload the page
|
||||
Then I see the subsection release date is 01/02/2103
|
||||
|
||||
@@ -64,19 +64,17 @@ def set_subsection_release_date_on_enter(_step, datestring, timestring): # pyli
|
||||
|
||||
|
||||
@step('I set the subsection due date to ([0-9/-]+)( [0-9:]+)?')
|
||||
def set_subsection_due_date(_step, datestring, timestring):
|
||||
def set_subsection_due_date(_step, datestring, timestring, key=None):
|
||||
if not world.css_visible('input#due_date'):
|
||||
world.css_click('.due-date-input .set-date')
|
||||
|
||||
set_subsection_date('input#due_date', datestring, 'input#due_time', timestring)
|
||||
assert world.css_visible('input#due_date')
|
||||
set_subsection_date('input#due_date', datestring, 'input#due_time', timestring, key)
|
||||
|
||||
|
||||
@step('I set the subsection due date on enter to ([0-9/-]+)( [0-9:]+)?')
|
||||
def set_subsection_due_date_on_enter(_step, datestring, timestring): # pylint: disable-msg=invalid-name
|
||||
if not world.css_visible('input#due_date'):
|
||||
world.css_click('.due-date-input .set-date')
|
||||
|
||||
set_subsection_date('input#due_date', datestring, 'input#due_time', timestring, 'ENTER')
|
||||
set_subsection_due_date(_step, datestring, timestring, 'ENTER')
|
||||
|
||||
|
||||
@step('I mark it as Homework$')
|
||||
|
||||
@@ -356,6 +356,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests the ajax callback to render an XModule
|
||||
"""
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
resp = self._test_preview(Location('i4x', 'edX', 'toy', 'vertical', 'vertical_test', None), 'container_preview')
|
||||
self.assertContains(resp, '/branch/draft/block/sample_video')
|
||||
self.assertContains(resp, '/branch/draft/block/separate_file_video')
|
||||
self.assertContains(resp, '/branch/draft/block/video_with_end_time')
|
||||
self.assertContains(resp, '/branch/draft/block/T1_changemind_poll_foo_2')
|
||||
|
||||
def _test_preview(self, location, view_name):
|
||||
""" Preview test case. """
|
||||
>>>>>>> edx/master
|
||||
direct_store = modulestore('direct')
|
||||
_, course_items = import_from_xml(direct_store, 'common/test/data/', ['toy'])
|
||||
usage_key = course_items[0].id.make_usage_key('vertical', 'vertical_test')
|
||||
|
||||
@@ -157,70 +157,7 @@ def unit_handler(request, usage_key_string):
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
component_templates = defaultdict(list)
|
||||
for category in COMPONENT_TYPES:
|
||||
component_class = _load_mixed_class(category)
|
||||
# add the default template
|
||||
# TODO: Once mixins are defined per-application, rather than per-runtime,
|
||||
# this should use a cms mixed-in class. (cpennington)
|
||||
if hasattr(component_class, 'display_name'):
|
||||
display_name = component_class.display_name.default or 'Blank'
|
||||
else:
|
||||
display_name = 'Blank'
|
||||
component_templates[category].append((
|
||||
display_name,
|
||||
category,
|
||||
False, # No defaults have markdown (hardcoded current default)
|
||||
None # no boilerplate for overrides
|
||||
))
|
||||
# add boilerplates
|
||||
if hasattr(component_class, 'templates'):
|
||||
for template in component_class.templates():
|
||||
filter_templates = getattr(component_class, 'filter_templates', None)
|
||||
if not filter_templates or filter_templates(template, course):
|
||||
component_templates[category].append((
|
||||
template['metadata'].get('display_name'),
|
||||
category,
|
||||
template['metadata'].get('markdown') is not None,
|
||||
template.get('template_id')
|
||||
))
|
||||
|
||||
# Check if there are any advanced modules specified in the course policy.
|
||||
# These modules should be specified as a list of strings, where the strings
|
||||
# are the names of the modules in ADVANCED_COMPONENT_TYPES that should be
|
||||
# enabled for the course.
|
||||
course_advanced_keys = course.advanced_modules
|
||||
|
||||
# Set component types according to course policy file
|
||||
if isinstance(course_advanced_keys, list):
|
||||
for category in course_advanced_keys:
|
||||
if category in ADVANCED_COMPONENT_TYPES:
|
||||
# Do I need to allow for boilerplates or just defaults on the
|
||||
# class? i.e., can an advanced have more than one entry in the
|
||||
# menu? one for default and others for prefilled boilerplates?
|
||||
try:
|
||||
component_class = _load_mixed_class(category)
|
||||
|
||||
component_templates['advanced'].append(
|
||||
(
|
||||
component_class.display_name.default or category,
|
||||
category,
|
||||
False,
|
||||
None # don't override default data
|
||||
)
|
||||
)
|
||||
except PluginMissingError:
|
||||
# dhm: I got this once but it can happen any time the
|
||||
# course author configures an advanced component which does
|
||||
# not exist on the server. This code here merely
|
||||
# prevents any authors from trying to instantiate the
|
||||
# non-existent component type by not showing it in the menu
|
||||
pass
|
||||
else:
|
||||
log.error(
|
||||
"Improper format for course advanced keys! %s",
|
||||
course_advanced_keys
|
||||
)
|
||||
component_templates = _get_component_templates(course)
|
||||
|
||||
xblocks = item.get_children()
|
||||
|
||||
@@ -259,9 +196,15 @@ def unit_handler(request, usage_key_string):
|
||||
return render_to_response('unit.html', {
|
||||
'context_course': course,
|
||||
'unit': item,
|
||||
<<<<<<< HEAD
|
||||
'unit_locator': usage_key,
|
||||
'xblocks': xblocks,
|
||||
'component_templates': component_templates,
|
||||
=======
|
||||
'unit_locator': locator,
|
||||
'locators': locators,
|
||||
'component_templates': json.dumps(component_templates),
|
||||
>>>>>>> edx/master
|
||||
'draft_preview_link': preview_lms_link,
|
||||
'published_preview_link': lms_link,
|
||||
'subsection': containing_subsection,
|
||||
@@ -301,6 +244,7 @@ def container_handler(request, usage_key_string):
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
component_templates = _get_component_templates(course)
|
||||
ancestor_xblocks = []
|
||||
parent = get_parent_xblock(xblock)
|
||||
while parent and parent.category != 'sequential':
|
||||
@@ -317,11 +261,106 @@ def container_handler(request, usage_key_string):
|
||||
'xblock_locator': usage_key,
|
||||
'unit': None if not ancestor_xblocks else ancestor_xblocks[0],
|
||||
'ancestor_xblocks': ancestor_xblocks,
|
||||
'component_templates': json.dumps(component_templates),
|
||||
})
|
||||
else:
|
||||
return HttpResponseBadRequest("Only supports html requests")
|
||||
|
||||
|
||||
def _get_component_templates(course):
|
||||
"""
|
||||
Returns the applicable component templates that can be used by the specified course.
|
||||
"""
|
||||
def create_template_dict(name, cat, boilerplate_name=None, is_common=False):
|
||||
"""
|
||||
Creates a component template dict.
|
||||
|
||||
Parameters
|
||||
display_name: the user-visible name of the component
|
||||
category: the type of component (problem, html, etc.)
|
||||
boilerplate_name: name of boilerplate for filling in default values. May be None.
|
||||
is_common: True if "common" problem, False if "advanced". May be None, as it is only used for problems.
|
||||
|
||||
"""
|
||||
return {
|
||||
"display_name": name,
|
||||
"category": cat,
|
||||
"boilerplate_name": boilerplate_name,
|
||||
"is_common": is_common
|
||||
}
|
||||
|
||||
component_templates = []
|
||||
# The component_templates array is in the order of "advanced" (if present), followed
|
||||
# by the components in the order listed in COMPONENT_TYPES.
|
||||
for category in COMPONENT_TYPES:
|
||||
templates_for_category = []
|
||||
component_class = _load_mixed_class(category)
|
||||
# add the default template
|
||||
# TODO: Once mixins are defined per-application, rather than per-runtime,
|
||||
# this should use a cms mixed-in class. (cpennington)
|
||||
if hasattr(component_class, 'display_name'):
|
||||
display_name = component_class.display_name.default or 'Blank'
|
||||
else:
|
||||
display_name = 'Blank'
|
||||
templates_for_category.append(create_template_dict(display_name, category))
|
||||
|
||||
# add boilerplates
|
||||
if hasattr(component_class, 'templates'):
|
||||
for template in component_class.templates():
|
||||
filter_templates = getattr(component_class, 'filter_templates', None)
|
||||
if not filter_templates or filter_templates(template, course):
|
||||
templates_for_category.append(
|
||||
create_template_dict(
|
||||
template['metadata'].get('display_name'),
|
||||
category,
|
||||
template.get('template_id'),
|
||||
template['metadata'].get('markdown') is not None
|
||||
)
|
||||
)
|
||||
component_templates.append({"type": category, "templates": templates_for_category})
|
||||
|
||||
# Check if there are any advanced modules specified in the course policy.
|
||||
# These modules should be specified as a list of strings, where the strings
|
||||
# are the names of the modules in ADVANCED_COMPONENT_TYPES that should be
|
||||
# enabled for the course.
|
||||
course_advanced_keys = course.advanced_modules
|
||||
advanced_component_templates = {"type": "advanced", "templates": []}
|
||||
# Set component types according to course policy file
|
||||
if isinstance(course_advanced_keys, list):
|
||||
for category in course_advanced_keys:
|
||||
if category in ADVANCED_COMPONENT_TYPES:
|
||||
# boilerplates not supported for advanced components
|
||||
try:
|
||||
component_class = _load_mixed_class(category)
|
||||
|
||||
advanced_component_templates['templates'].append(
|
||||
create_template_dict(
|
||||
component_class.display_name.default or category,
|
||||
category
|
||||
)
|
||||
)
|
||||
except PluginMissingError:
|
||||
# dhm: I got this once but it can happen any time the
|
||||
# course author configures an advanced component which does
|
||||
# not exist on the server. This code here merely
|
||||
# prevents any authors from trying to instantiate the
|
||||
# non-existent component type by not showing it in the menu
|
||||
log.warning(
|
||||
"Advanced component %s does not exist. It will not be added to the Studio new component menu.",
|
||||
category
|
||||
)
|
||||
pass
|
||||
else:
|
||||
log.error(
|
||||
"Improper format for course advanced keys! %s",
|
||||
course_advanced_keys
|
||||
)
|
||||
if len(advanced_component_templates['templates']) > 0:
|
||||
component_templates.insert(0, advanced_component_templates)
|
||||
|
||||
return component_templates
|
||||
|
||||
|
||||
@login_required
|
||||
def _get_item_in_course(request, usage_key):
|
||||
"""
|
||||
|
||||
@@ -9,7 +9,9 @@ from contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
__all__ = ['edge', 'event', 'landing']
|
||||
|
||||
EDITING_TEMPLATES = [
|
||||
"basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button", "upload-dialog", "image-modal"
|
||||
"basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button", "upload-dialog", "image-modal",
|
||||
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
|
||||
"add-xblock-component-menu-problem"
|
||||
]
|
||||
|
||||
# points to the temporary course landing page with log in and sign up
|
||||
@@ -37,11 +39,20 @@ def render_from_lms(template_name, dictionary, context=None, namespace='main'):
|
||||
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
|
||||
|
||||
|
||||
def _xmodule_recurse(item, action):
|
||||
for child in item.get_children():
|
||||
_xmodule_recurse(child, action)
|
||||
def _xmodule_recurse(item, action, ignore_exception=()):
|
||||
"""
|
||||
Recursively apply provided action on item and its children
|
||||
|
||||
action(item)
|
||||
ignore_exception (Exception Object): A optional argument; when passed ignores the corresponding
|
||||
exception raised during xmodule recursion,
|
||||
"""
|
||||
for child in item.get_children():
|
||||
_xmodule_recurse(child, action, ignore_exception)
|
||||
|
||||
try:
|
||||
return action(item)
|
||||
except ignore_exception:
|
||||
return
|
||||
|
||||
|
||||
def get_parent_xblock(xblock):
|
||||
@@ -58,40 +69,54 @@ def get_parent_xblock(xblock):
|
||||
return modulestore().get_item(parent_locations[0])
|
||||
|
||||
|
||||
def _xblock_has_studio_page(xblock):
|
||||
def is_unit(xblock):
|
||||
"""
|
||||
Returns true if the specified xblock is a vertical that is treated as a unit.
|
||||
A unit is a vertical that is a direct child of a sequential (aka a subsection).
|
||||
"""
|
||||
if xblock.category == 'vertical':
|
||||
parent_xblock = get_parent_xblock(xblock)
|
||||
parent_category = parent_xblock.category if parent_xblock else None
|
||||
return parent_category == 'sequential'
|
||||
return False
|
||||
|
||||
|
||||
def xblock_has_own_studio_page(xblock):
|
||||
"""
|
||||
Returns true if the specified xblock has an associated Studio page. Most xblocks do
|
||||
not have their own page but are instead shown on the page of their parent. There
|
||||
are a few exceptions:
|
||||
1. Courses
|
||||
2. Verticals
|
||||
2. Verticals that are either:
|
||||
- themselves treated as units (in which case they are shown on a unit page)
|
||||
- a direct child of a unit (in which case they are shown on a container page)
|
||||
3. XBlocks with children, except for:
|
||||
- subsections (aka sequential blocks)
|
||||
- chapters
|
||||
- sequentials (aka subsections)
|
||||
- chapters (aka sections)
|
||||
"""
|
||||
category = xblock.category
|
||||
if category in ('course', 'vertical'):
|
||||
|
||||
if is_unit(xblock):
|
||||
return True
|
||||
elif category == 'vertical':
|
||||
parent_xblock = get_parent_xblock(xblock)
|
||||
return is_unit(parent_xblock) if parent_xblock else False
|
||||
elif category in ('sequential', 'chapter'):
|
||||
return False
|
||||
elif xblock.has_children:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# All other xblocks with children have their own page
|
||||
return xblock.has_children
|
||||
|
||||
|
||||
def xblock_studio_url(xblock):
|
||||
"""
|
||||
Returns the Studio editing URL for the specified xblock.
|
||||
"""
|
||||
if not _xblock_has_studio_page(xblock):
|
||||
if not xblock_has_own_studio_page(xblock):
|
||||
return None
|
||||
category = xblock.category
|
||||
parent_xblock = get_parent_xblock(xblock)
|
||||
if parent_xblock:
|
||||
parent_category = parent_xblock.category
|
||||
else:
|
||||
parent_category = None
|
||||
parent_category = parent_xblock.category if parent_xblock else None
|
||||
if category == 'course':
|
||||
return reverse_course_url('course_handler', xblock.location.course_key)
|
||||
elif category == 'vertical' and parent_category == 'sequential':
|
||||
|
||||
@@ -20,8 +20,13 @@ from xblock.fields import Scope
|
||||
from xblock.fragment import Fragment
|
||||
|
||||
import xmodule
|
||||
<<<<<<< HEAD
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
=======
|
||||
from xmodule.modulestore.django import modulestore, loc_mapper
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError
|
||||
>>>>>>> edx/master
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.video_module import manage_video_subtitles_save
|
||||
|
||||
@@ -31,7 +36,7 @@ from util.string_utils import str_to_bool
|
||||
from ..utils import get_modulestore
|
||||
|
||||
from .access import has_course_access
|
||||
from .helpers import _xmodule_recurse
|
||||
from .helpers import _xmodule_recurse, xblock_has_own_studio_page
|
||||
from contentstore.utils import compute_publish_state, PublishState
|
||||
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
|
||||
from contentstore.views.preview import get_preview_fragment
|
||||
@@ -177,6 +182,7 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
accept_header = request.META.get('HTTP_ACCEPT', 'application/json')
|
||||
|
||||
if 'application/json' in accept_header:
|
||||
<<<<<<< HEAD
|
||||
store = get_modulestore(usage_key)
|
||||
component = store.get_item(usage_key)
|
||||
is_read_only = _xblock_is_read_only(component)
|
||||
@@ -184,40 +190,64 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
# wrap the generated fragment in the xmodule_editor div so that the javascript
|
||||
# can bind to it correctly
|
||||
component.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime', usage_id_serializer=unicode))
|
||||
=======
|
||||
store = get_modulestore(old_location)
|
||||
xblock = store.get_item(old_location)
|
||||
is_read_only = _is_xblock_read_only(xblock)
|
||||
container_views = ['container_preview', 'reorderable_container_child_preview']
|
||||
unit_views = ['student_view']
|
||||
|
||||
# wrap the generated fragment in the xmodule_editor div so that the javascript
|
||||
# can bind to it correctly
|
||||
xblock.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime'))
|
||||
>>>>>>> edx/master
|
||||
|
||||
if view_name == 'studio_view':
|
||||
try:
|
||||
fragment = component.render('studio_view')
|
||||
fragment = xblock.render('studio_view')
|
||||
# catch exceptions indiscriminately, since after this point they escape the
|
||||
# dungeon and surface as uneditable, unsaveable, and undeletable
|
||||
# component-goblins.
|
||||
except Exception as exc: # pylint: disable=w0703
|
||||
log.debug("unable to render studio_view for %r", component, exc_info=True)
|
||||
log.debug("unable to render studio_view for %r", xblock, exc_info=True)
|
||||
fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
|
||||
|
||||
# change not authored by requestor but by xblocks.
|
||||
store.update_item(component, None)
|
||||
store.update_item(xblock, None)
|
||||
|
||||
elif view_name == 'student_view' and component.has_children:
|
||||
elif view_name == 'student_view' and xblock_has_own_studio_page(xblock):
|
||||
context = {
|
||||
'runtime_type': 'studio',
|
||||
'container_view': False,
|
||||
'read_only': is_read_only,
|
||||
'root_xblock': component,
|
||||
'root_xblock': xblock,
|
||||
}
|
||||
# For non-leaf xblocks on the unit page, show the special rendering
|
||||
# which links to the new container page.
|
||||
html = render_to_string('container_xblock_component.html', {
|
||||
'xblock_context': context,
|
||||
<<<<<<< HEAD
|
||||
'xblock': component,
|
||||
'locator': usage_key,
|
||||
=======
|
||||
'xblock': xblock,
|
||||
'locator': locator,
|
||||
>>>>>>> edx/master
|
||||
})
|
||||
return JsonResponse({
|
||||
'html': html,
|
||||
'resources': [],
|
||||
})
|
||||
elif view_name in ('student_view', 'container_preview'):
|
||||
is_container_view = (view_name == 'container_preview')
|
||||
elif view_name in (unit_views + container_views):
|
||||
is_container_view = (view_name in container_views)
|
||||
|
||||
# Determine the items to be shown as reorderable. Note that the view
|
||||
# 'reorderable_container_child_preview' is only rendered for xblocks that
|
||||
# are being shown in a reorderable container, so the xblock is automatically
|
||||
# added to the list.
|
||||
reorderable_items = set()
|
||||
if view_name == 'reorderable_container_child_preview':
|
||||
reorderable_items.add(xblock.location)
|
||||
|
||||
# Only show the new style HTML for the container view, i.e. for non-verticals
|
||||
# Note: this special case logic can be removed once the unit page is replaced
|
||||
@@ -226,10 +256,11 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
'runtime_type': 'studio',
|
||||
'container_view': is_container_view,
|
||||
'read_only': is_read_only,
|
||||
'root_xblock': component,
|
||||
'root_xblock': xblock if (view_name == 'container_preview') else None,
|
||||
'reorderable_items': reorderable_items
|
||||
}
|
||||
|
||||
fragment = get_preview_fragment(request, component, context)
|
||||
fragment = get_preview_fragment(request, xblock, context)
|
||||
# For old-style pages (such as unit and static pages), wrap the preview with
|
||||
# the component div. Note that the container view recursively adds headers
|
||||
# into the preview fragment, so we don't want to add another header here.
|
||||
@@ -237,7 +268,7 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
fragment.content = render_to_string('component.html', {
|
||||
'xblock_context': context,
|
||||
'preview': fragment.content,
|
||||
'label': component.display_name or component.scope_ids.block_type,
|
||||
'label': xblock.display_name or xblock.scope_ids.block_type,
|
||||
})
|
||||
else:
|
||||
raise Http404
|
||||
@@ -255,7 +286,7 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
return HttpResponse(status=406)
|
||||
|
||||
|
||||
def _xblock_is_read_only(xblock):
|
||||
def _is_xblock_read_only(xblock):
|
||||
"""
|
||||
Returns true if the specified xblock is read-only, meaning that it cannot be edited.
|
||||
"""
|
||||
@@ -293,11 +324,19 @@ def _save_item(request, usage_key, data=None, children=None, metadata=None, null
|
||||
|
||||
if publish:
|
||||
if publish == 'make_private':
|
||||
_xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
|
||||
_xmodule_recurse(
|
||||
existing_item,
|
||||
lambda i: modulestore().unpublish(i.location),
|
||||
ignore_exception=ItemNotFoundError
|
||||
)
|
||||
elif publish == 'create_draft':
|
||||
# This recursively clones the existing item location to a draft location (the draft is
|
||||
# implicit, because modulestore is a Draft modulestore)
|
||||
_xmodule_recurse(existing_item, lambda i: modulestore().convert_to_draft(i.location))
|
||||
_xmodule_recurse(
|
||||
existing_item,
|
||||
lambda i: modulestore().convert_to_draft(i.location),
|
||||
ignore_exception=DuplicateItemError
|
||||
)
|
||||
|
||||
if data:
|
||||
# TODO Allow any scope.content fields not just "data" (exactly like the get below this)
|
||||
@@ -393,7 +432,7 @@ def _create_item(request):
|
||||
metadata = {}
|
||||
data = None
|
||||
template_id = request.json.get('boilerplate')
|
||||
if template_id is not None:
|
||||
if template_id:
|
||||
clz = parent.runtime.load_block_type(category)
|
||||
if clz is not None:
|
||||
template = clz.get_template(template_id)
|
||||
|
||||
@@ -27,7 +27,12 @@ from util.sandboxing import can_execute_unsafe_code
|
||||
|
||||
import static_replace
|
||||
from .session_kv_store import SessionKeyValueStore
|
||||
<<<<<<< HEAD
|
||||
from .helpers import render_from_lms
|
||||
=======
|
||||
from .helpers import render_from_lms, xblock_has_own_studio_page
|
||||
from ..utils import get_course_for_item
|
||||
>>>>>>> edx/master
|
||||
|
||||
from contentstore.views.access import get_user_role
|
||||
|
||||
@@ -156,6 +161,13 @@ def _load_preview_module(request, descriptor):
|
||||
return descriptor
|
||||
|
||||
|
||||
def _is_xblock_reorderable(xblock, context):
|
||||
"""
|
||||
Returns true if the specified xblock is in the set of reorderable xblocks.
|
||||
"""
|
||||
return xblock.location in context['reorderable_items']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
"""
|
||||
@@ -163,15 +175,23 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
"""
|
||||
# Only add the Studio wrapper when on the container page. The unit page will remain as is for now.
|
||||
if context.get('container_view', None) and view == 'student_view':
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
root_xblock = context.get('root_xblock')
|
||||
is_root = root_xblock and xblock.location == root_xblock.location
|
||||
locator = loc_mapper().translate_location(xblock.course_id, xblock.location, published=False)
|
||||
is_reorderable = _is_xblock_reorderable(xblock, context)
|
||||
>>>>>>> edx/master
|
||||
template_context = {
|
||||
'xblock_context': context,
|
||||
'xblock': xblock,
|
||||
'content': frag.content,
|
||||
'is_root': is_root,
|
||||
'is_reorderable': is_reorderable,
|
||||
}
|
||||
if xblock.category == 'vertical':
|
||||
template = 'studio_vertical_wrapper.html'
|
||||
elif xblock.location != context.get('root_xblock').location and xblock.has_children:
|
||||
template = 'container_xblock_component.html'
|
||||
# For child xblocks with their own page, render a link to the page
|
||||
if xblock_has_own_studio_page(xblock) and not is_root:
|
||||
template = 'studio_container_wrapper.html'
|
||||
else:
|
||||
template = 'studio_xblock_wrapper.html'
|
||||
html = render_to_string(template, template_context)
|
||||
|
||||
158
cms/djangoapps/contentstore/views/tests/test_container_page.py
Normal file
158
cms/djangoapps/contentstore/views/tests/test_container_page.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
Unit tests for the container page.
|
||||
"""
|
||||
|
||||
from contentstore.utils import compute_publish_state, PublishState
|
||||
from contentstore.views.tests.utils import StudioPageTestCase
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
|
||||
|
||||
class ContainerPageTestCase(StudioPageTestCase):
|
||||
"""
|
||||
Unit tests for the container page.
|
||||
"""
|
||||
|
||||
container_view = 'container_preview'
|
||||
reorderable_child_view = 'reorderable_container_child_preview'
|
||||
|
||||
def setUp(self):
|
||||
super(ContainerPageTestCase, self).setUp()
|
||||
self.vertical = ItemFactory.create(parent_location=self.sequential.location,
|
||||
category='vertical', display_name='Unit')
|
||||
self.html = ItemFactory.create(parent_location=self.vertical.location,
|
||||
category="html", display_name="HTML")
|
||||
self.child_container = ItemFactory.create(parent_location=self.vertical.location,
|
||||
category='split_test', display_name='Split Test')
|
||||
self.child_vertical = ItemFactory.create(parent_location=self.child_container.location,
|
||||
category='vertical', display_name='Child Vertical')
|
||||
self.video = ItemFactory.create(parent_location=self.child_vertical.location,
|
||||
category="video", display_name="My Video")
|
||||
|
||||
def test_container_html(self):
|
||||
branch_name = "MITx.999.Robot_Super_Course/branch/draft/block"
|
||||
self._test_html_content(
|
||||
self.child_container,
|
||||
branch_name=branch_name,
|
||||
expected_section_tag=(
|
||||
'<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" '
|
||||
'data-locator="{branch_name}/Split_Test">'.format(branch_name=branch_name)
|
||||
),
|
||||
expected_breadcrumbs=(
|
||||
r'<a href="/unit/{branch_name}/Unit"\s*'
|
||||
r'class="navigation-link navigation-parent">Unit</a>\s*'
|
||||
r'<a href="#" class="navigation-link navigation-current">Split Test</a>'
|
||||
).format(branch_name=branch_name)
|
||||
)
|
||||
|
||||
def test_container_on_container_html(self):
|
||||
"""
|
||||
Create the scenario of an xblock with children (non-vertical) on the container page.
|
||||
This should create a container page that is a child of another container page.
|
||||
"""
|
||||
published_container = ItemFactory.create(
|
||||
parent_location=self.child_container.location,
|
||||
category="wrapper", display_name="Wrapper"
|
||||
)
|
||||
ItemFactory.create(
|
||||
parent_location=published_container.location,
|
||||
category="html", display_name="Child HTML"
|
||||
)
|
||||
|
||||
def test_container_html(xblock):
|
||||
branch_name = "MITx.999.Robot_Super_Course/branch/draft/block"
|
||||
self._test_html_content(
|
||||
xblock,
|
||||
branch_name=branch_name,
|
||||
expected_section_tag=(
|
||||
'<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" '
|
||||
'data-locator="{branch_name}/Wrapper">'.format(branch_name=branch_name)
|
||||
),
|
||||
expected_breadcrumbs=(
|
||||
r'<a href="/unit/{branch_name}/Unit"\s*'
|
||||
r'class="navigation-link navigation-parent">Unit</a>\s*'
|
||||
r'<a href="/container/{branch_name}/Split_Test"\s*'
|
||||
r'class="navigation-link navigation-parent">Split Test</a>\s*'
|
||||
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'
|
||||
).format(branch_name=branch_name)
|
||||
)
|
||||
|
||||
# Test the published version of the container
|
||||
test_container_html(published_container)
|
||||
|
||||
# Now make the unit and its children into a draft and validate the container again
|
||||
modulestore('draft').convert_to_draft(self.vertical.location)
|
||||
modulestore('draft').convert_to_draft(self.child_vertical.location)
|
||||
draft_container = modulestore('draft').convert_to_draft(published_container.location)
|
||||
test_container_html(draft_container)
|
||||
|
||||
def _test_html_content(self, xblock, branch_name, expected_section_tag, expected_breadcrumbs):
|
||||
"""
|
||||
Get the HTML for a container page and verify the section tag is correct
|
||||
and the breadcrumbs trail is correct.
|
||||
"""
|
||||
html = self.get_page_html(xblock)
|
||||
publish_state = compute_publish_state(xblock)
|
||||
self.assertIn(expected_section_tag, html)
|
||||
# Verify the navigation link at the top of the page is correct.
|
||||
self.assertRegexpMatches(html, expected_breadcrumbs)
|
||||
|
||||
# Verify the link that allows users to change publish status.
|
||||
expected_message = None
|
||||
if publish_state == PublishState.public:
|
||||
expected_message = 'you need to edit unit <a href="/unit/{branch_name}/Unit">Unit</a> as a draft.'
|
||||
else:
|
||||
expected_message = 'your changes will be published with unit <a href="/unit/{branch_name}/Unit">Unit</a>.'
|
||||
expected_unit_link = expected_message.format(
|
||||
branch_name=branch_name
|
||||
)
|
||||
self.assertIn(expected_unit_link, html)
|
||||
|
||||
def test_public_container_preview_html(self):
|
||||
"""
|
||||
Verify that a public xblock's container preview returns the expected HTML.
|
||||
"""
|
||||
self.validate_preview_html(self.vertical, self.container_view,
|
||||
can_edit=False, can_reorder=False, can_add=False)
|
||||
self.validate_preview_html(self.child_container, self.container_view,
|
||||
can_edit=False, can_reorder=False, can_add=False)
|
||||
self.validate_preview_html(self.child_vertical, self.reorderable_child_view,
|
||||
can_edit=False, can_reorder=False, can_add=False)
|
||||
|
||||
def test_draft_container_preview_html(self):
|
||||
"""
|
||||
Verify that a draft xblock's container preview returns the expected HTML.
|
||||
"""
|
||||
draft_unit = modulestore('draft').convert_to_draft(self.vertical.location)
|
||||
draft_child_container = modulestore('draft').convert_to_draft(self.child_container.location)
|
||||
draft_child_vertical = modulestore('draft').convert_to_draft(self.child_vertical.location)
|
||||
self.validate_preview_html(draft_unit, self.container_view,
|
||||
can_edit=True, can_reorder=True, can_add=True)
|
||||
self.validate_preview_html(draft_child_container, self.container_view,
|
||||
can_edit=True, can_reorder=True, can_add=True)
|
||||
self.validate_preview_html(draft_child_vertical, self.reorderable_child_view,
|
||||
can_edit=True, can_reorder=True, can_add=True)
|
||||
|
||||
def test_public_child_container_preview_html(self):
|
||||
"""
|
||||
Verify that a public container rendered as a child of the container page returns the expected HTML.
|
||||
"""
|
||||
empty_child_container = ItemFactory.create(parent_location=self.vertical.location,
|
||||
category='split_test', display_name='Split Test')
|
||||
ItemFactory.create(parent_location=empty_child_container.location,
|
||||
category='html', display_name='Split Child')
|
||||
self.validate_preview_html(empty_child_container, self.reorderable_child_view,
|
||||
can_reorder=False, can_edit=False, can_add=False)
|
||||
|
||||
def test_draft_child_container_preview_html(self):
|
||||
"""
|
||||
Verify that a draft container rendered as a child of the container page returns the expected HTML.
|
||||
"""
|
||||
empty_child_container = ItemFactory.create(parent_location=self.vertical.location,
|
||||
category='split_test', display_name='Split Test')
|
||||
ItemFactory.create(parent_location=empty_child_container.location,
|
||||
category='html', display_name='Split Child')
|
||||
modulestore('draft').convert_to_draft(self.vertical.location)
|
||||
draft_empty_child_container = modulestore('draft').convert_to_draft(empty_child_container.location)
|
||||
self.validate_preview_html(draft_empty_child_container, self.reorderable_child_view,
|
||||
can_reorder=True, can_edit=False, can_add=False)
|
||||
@@ -612,6 +612,80 @@ class TestEditItem(ItemTest):
|
||||
draft = self.get_item_from_modulestore(self.problem_usage_key, True)
|
||||
self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|
||||
|
||||
def test_create_draft_with_multiple_requests(self):
|
||||
"""
|
||||
Create a draft request returns already created version if it exists.
|
||||
"""
|
||||
# Make problem public.
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
# Now make it draft, which means both versions will exist.
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={
|
||||
'publish': 'create_draft'
|
||||
}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
draft_1 = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
self.assertIsNotNone(draft_1)
|
||||
|
||||
# Now check that when a user sends request to create a draft when there is already a draft version then
|
||||
# user gets that already created draft instead of getting 'DuplicateItemError' exception.
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={
|
||||
'publish': 'create_draft'
|
||||
}
|
||||
)
|
||||
draft_2 = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
self.assertIsNotNone(draft_2)
|
||||
self.assertEqual(draft_1, draft_2)
|
||||
|
||||
|
||||
def test_make_private_with_multiple_requests(self):
|
||||
"""
|
||||
Make private requests gets proper response even if xmodule is already made private.
|
||||
"""
|
||||
# Make problem public.
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={'publish': 'make_public'}
|
||||
)
|
||||
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_locator, False))
|
||||
|
||||
# Now make it private, and check that its published version not exists
|
||||
resp = self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={
|
||||
'publish': 'make_private'
|
||||
}
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self.get_item_from_modulestore(self.problem_locator, False)
|
||||
draft_1 = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
self.assertIsNotNone(draft_1)
|
||||
|
||||
# Now check that when a user sends request to make it private when it already is private then
|
||||
# user gets that private version instead of getting 'ItemNotFoundError' exception.
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={
|
||||
'publish': 'make_private'
|
||||
}
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
with self.assertRaises(ItemNotFoundError):
|
||||
self.get_item_from_modulestore(self.problem_locator, False)
|
||||
draft_2 = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
self.assertIsNotNone(draft_2)
|
||||
self.assertEqual(draft_1, draft_2)
|
||||
|
||||
|
||||
def test_published_and_draft_contents_with_update(self):
|
||||
""" Create a draft and publish it then modify the draft and check that published content is not modified """
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ class TabsPageTests(CourseTestCase):
|
||||
self.assertIn('<span class="action-button-text">Edit</span>', html)
|
||||
self.assertIn('<span class="sr">Duplicate this component</span>', html)
|
||||
self.assertIn('<span class="sr">Delete this component</span>', html)
|
||||
self.assertIn('<span data-tooltip="Drag to reorder" class="drag-handle"></span>', html)
|
||||
self.assertIn('<span data-tooltip="Drag to reorder" class="drag-handle action"></span>', html)
|
||||
|
||||
|
||||
|
||||
|
||||
77
cms/djangoapps/contentstore/views/tests/test_unit_page.py
Normal file
77
cms/djangoapps/contentstore/views/tests/test_unit_page.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Unit tests for the unit page.
|
||||
"""
|
||||
|
||||
from contentstore.views.tests.utils import StudioPageTestCase
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
|
||||
|
||||
class UnitPageTestCase(StudioPageTestCase):
|
||||
"""
|
||||
Unit tests for the unit page.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(UnitPageTestCase, self).setUp()
|
||||
self.vertical = ItemFactory.create(parent_location=self.sequential.location,
|
||||
category='vertical', display_name='Unit')
|
||||
self.video = ItemFactory.create(parent_location=self.vertical.location,
|
||||
category="video", display_name="My Video")
|
||||
|
||||
def test_public_unit_page_html(self):
|
||||
"""
|
||||
Verify that an xblock returns the expected HTML for a public unit page.
|
||||
"""
|
||||
html = self.get_page_html(self.vertical)
|
||||
self.validate_html_for_add_buttons(html)
|
||||
|
||||
def test_draft_unit_page_html(self):
|
||||
"""
|
||||
Verify that an xblock returns the expected HTML for a draft unit page.
|
||||
"""
|
||||
draft_unit = modulestore('draft').convert_to_draft(self.vertical.location)
|
||||
html = self.get_page_html(draft_unit)
|
||||
self.validate_html_for_add_buttons(html)
|
||||
|
||||
def test_public_component_preview_html(self):
|
||||
"""
|
||||
Verify that a public xblock's preview returns the expected HTML.
|
||||
"""
|
||||
self.validate_preview_html(self.video, 'student_view',
|
||||
can_edit=True, can_reorder=True, can_add=False)
|
||||
|
||||
def test_draft_component_preview_html(self):
|
||||
"""
|
||||
Verify that a draft xblock's preview returns the expected HTML.
|
||||
"""
|
||||
modulestore('draft').convert_to_draft(self.vertical.location)
|
||||
draft_video = modulestore('draft').convert_to_draft(self.video.location)
|
||||
self.validate_preview_html(draft_video, 'student_view',
|
||||
can_edit=True, can_reorder=True, can_add=False)
|
||||
|
||||
def test_public_child_container_preview_html(self):
|
||||
"""
|
||||
Verify that a public child container rendering on the unit page (which shows a View arrow
|
||||
to the container page) returns the expected HTML.
|
||||
"""
|
||||
child_container = ItemFactory.create(parent_location=self.vertical.location,
|
||||
category='split_test', display_name='Split Test')
|
||||
ItemFactory.create(parent_location=child_container.location,
|
||||
category='html', display_name='grandchild')
|
||||
self.validate_preview_html(child_container, 'student_view',
|
||||
can_reorder=True, can_edit=False, can_add=False)
|
||||
|
||||
def test_draft_child_container_preview_html(self):
|
||||
"""
|
||||
Verify that a draft child container rendering on the unit page (which shows a View arrow
|
||||
to the container page) returns the expected HTML.
|
||||
"""
|
||||
child_container = ItemFactory.create(parent_location=self.vertical.location,
|
||||
category='split_test', display_name='Split Test')
|
||||
ItemFactory.create(parent_location=child_container.location,
|
||||
category='html', display_name='grandchild')
|
||||
modulestore('draft').convert_to_draft(self.vertical.location)
|
||||
draft_child_container = modulestore('draft').get_item(child_container.location)
|
||||
self.validate_preview_html(draft_child_container, 'student_view',
|
||||
can_reorder=True, can_edit=False, can_add=False)
|
||||
81
cms/djangoapps/contentstore/views/tests/utils.py
Normal file
81
cms/djangoapps/contentstore/views/tests/utils.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Utilities for view tests.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.views.helpers import xblock_studio_url
|
||||
from xmodule.modulestore.django import loc_mapper
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
|
||||
|
||||
class StudioPageTestCase(CourseTestCase):
|
||||
"""
|
||||
Base class for all tests of Studio pages.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(StudioPageTestCase, self).setUp()
|
||||
self.chapter = ItemFactory.create(parent_location=self.course.location,
|
||||
category='chapter', display_name="Week 1")
|
||||
self.sequential = ItemFactory.create(parent_location=self.chapter.location,
|
||||
category='sequential', display_name="Lesson 1")
|
||||
|
||||
def get_page_html(self, xblock):
|
||||
"""
|
||||
Returns the HTML for the page representing the xblock.
|
||||
"""
|
||||
url = xblock_studio_url(xblock, self.course)
|
||||
self.assertIsNotNone(url)
|
||||
resp = self.client.get_html(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
return resp.content
|
||||
|
||||
def get_preview_html(self, xblock, view_name):
|
||||
"""
|
||||
Returns the HTML for the xblock when shown within a unit or container page.
|
||||
"""
|
||||
locator = loc_mapper().translate_location(self.course.id, xblock.location, published=False)
|
||||
preview_url = '/xblock/{locator}/{view_name}'.format(locator=locator, view_name=view_name)
|
||||
resp = self.client.get_json(preview_url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
resp_content = json.loads(resp.content)
|
||||
return resp_content['html']
|
||||
|
||||
def validate_preview_html(self, xblock, view_name, can_edit=True, can_reorder=True, can_add=True):
|
||||
"""
|
||||
Verify that the specified xblock's preview has the expected HTML elements.
|
||||
"""
|
||||
html = self.get_preview_html(xblock, view_name)
|
||||
self.validate_html_for_add_buttons(html, can_add=can_add)
|
||||
|
||||
# Verify that there are no drag handles for public blocks
|
||||
drag_handle_html = '<span data-tooltip="Drag to reorder" class="drag-handle action"></span>'
|
||||
if can_reorder:
|
||||
self.assertIn(drag_handle_html, html)
|
||||
else:
|
||||
self.assertNotIn(drag_handle_html, html)
|
||||
|
||||
# Verify that there are no action buttons for public blocks
|
||||
expected_button_html = [
|
||||
'<a href="#" class="edit-button action-button">',
|
||||
'<a href="#" data-tooltip="Delete" class="delete-button action-button">',
|
||||
'<a href="#" data-tooltip="Duplicate" class="duplicate-button action-button">'
|
||||
]
|
||||
for button_html in expected_button_html:
|
||||
if can_edit:
|
||||
self.assertIn(button_html, html)
|
||||
else:
|
||||
self.assertNotIn(button_html, html)
|
||||
|
||||
def validate_html_for_add_buttons(self, html, can_add=True):
|
||||
"""
|
||||
Validate that the specified HTML has the appropriate add actions for the current publish state.
|
||||
"""
|
||||
# Verify that there are no add buttons for public blocks
|
||||
add_button_html = '<div class="add-xblock-component new-component-item adding"></div>'
|
||||
if can_add:
|
||||
self.assertIn(add_button_html, html)
|
||||
else:
|
||||
self.assertNotIn(add_button_html, html)
|
||||
Reference in New Issue
Block a user