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:
Calen Pennington
2014-05-22 10:52:00 -04:00
178 changed files with 4916 additions and 3629 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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$')

View File

@@ -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')

View File

@@ -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):
"""

View File

@@ -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':

View File

@@ -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)

View File

@@ -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)

View 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)

View File

@@ -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 """

View File

@@ -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)

View 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)

View 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)