Replace unit page with the container page.
STUD-1754
This commit is contained in:
@@ -397,27 +397,3 @@ def create_other_user(_step, name, has_extra_perms, role_name):
|
||||
def log_out(_step):
|
||||
world.visit('logout')
|
||||
|
||||
|
||||
@step(u'I click on "edit a draft"$')
|
||||
def i_edit_a_draft(_step):
|
||||
world.css_click("a.create-draft")
|
||||
|
||||
|
||||
@step(u'I click on "replace with draft"$')
|
||||
def i_replace_w_draft(_step):
|
||||
world.css_click("a.publish-draft")
|
||||
|
||||
|
||||
@step(u'I click on "delete draft"$')
|
||||
def i_delete_draft(_step):
|
||||
world.css_click("a.delete-draft")
|
||||
|
||||
|
||||
@step(u'I publish the unit$')
|
||||
def publish_unit(_step):
|
||||
world.select_option('visibility-select', 'public')
|
||||
|
||||
|
||||
@step(u'I unpublish the unit$')
|
||||
def unpublish_unit(_step):
|
||||
world.select_option('visibility-select', 'private')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# pylint: disable=W0613
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_in # pylint: disable=E0611
|
||||
from nose.tools import assert_true, assert_in, assert_equal # pylint: disable=E0611
|
||||
|
||||
DISPLAY_NAME = "Display Name"
|
||||
|
||||
@@ -48,7 +48,7 @@ def add_a_multi_step_component(step, is_advanced, category):
|
||||
def see_a_multi_step_component(step, category):
|
||||
|
||||
# Wait for all components to finish rendering
|
||||
selector = 'li.component div.xblock-student_view'
|
||||
selector = 'li.studio-xblock-wrapper div.xblock-student_view'
|
||||
world.wait_for(lambda _: len(world.css_find(selector)) == len(step.hashes))
|
||||
|
||||
for idx, step_hash in enumerate(step.hashes):
|
||||
@@ -79,7 +79,7 @@ def see_a_problem_component(step, category):
|
||||
assert_true(world.is_css_present(component_css),
|
||||
'No problem was added to the unit.')
|
||||
|
||||
problem_css = 'li.component div.xblock-student_view'
|
||||
problem_css = 'li.studio-xblock-wrapper div.xblock-student_view'
|
||||
actual_text = world.css_text(problem_css)
|
||||
assert_in(category.upper(), actual_text)
|
||||
|
||||
@@ -93,7 +93,7 @@ def add_component_category(step, component, category):
|
||||
|
||||
@step(u'I delete all components$')
|
||||
def delete_all_components(step):
|
||||
count = len(world.css_find('ol.components li.component'))
|
||||
count = len(world.css_find('ol.reorderable-container li.studio-xblock-wrapper'))
|
||||
step.given('I delete "' + str(count) + '" component')
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ def delete_components(step, number):
|
||||
|
||||
@step(u'I see no components')
|
||||
def see_no_components(steps):
|
||||
assert world.is_css_not_present('li.component')
|
||||
assert world.is_css_not_present('li.studio-xblock-wrapper')
|
||||
|
||||
|
||||
@step(u'I delete a component')
|
||||
@@ -162,8 +162,9 @@ def see_component_in_position(step, display_name, index):
|
||||
|
||||
@step(u'I see the display name is "([^"]*)"')
|
||||
def check_component_display_name(step, display_name):
|
||||
label = world.css_text(".component-header")
|
||||
assert display_name == label
|
||||
# The display name for the unit uses the same structure, must differentiate by level-element.
|
||||
label = world.css_html("section.level-element>header>div>div>span.xblock-display-name")
|
||||
assert_equal(display_name, label)
|
||||
|
||||
|
||||
@step(u'I change the display name to "([^"]*)"')
|
||||
|
||||
@@ -122,9 +122,9 @@ def ensure_settings_visible():
|
||||
|
||||
|
||||
@world.absorb
|
||||
def edit_component():
|
||||
def edit_component(index=0):
|
||||
world.wait_for(lambda _driver: world.css_visible('a.edit-button'))
|
||||
world.css_click('a.edit-button')
|
||||
world.css_click('a.edit-button', index)
|
||||
world.wait_for_ajax_complete()
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ def i_click_on_error_dialog(step):
|
||||
# we don't know the actual ID of the vertical. So just check that we did go to a
|
||||
# vertical page in the course (there should only be one).
|
||||
vertical_usage_key = course_key.make_usage_key("vertical", None)
|
||||
vertical_url = reverse_usage_url('unit_handler', vertical_usage_key)
|
||||
vertical_url = reverse_usage_url('container_handler', vertical_usage_key)
|
||||
# Remove the trailing "/None" from the URL - we don't know the course ID, so we just want to
|
||||
# check that we visited a vertical URL.
|
||||
if vertical_url.endswith("/None"):
|
||||
|
||||
@@ -81,38 +81,6 @@ Feature: CMS.Problem Editor
|
||||
When I edit and select Settings
|
||||
Then Edit High Level Source is visible
|
||||
|
||||
# This is a very specific scenario that was failing with some of the
|
||||
# DB rearchitecture changes. It had to do with children IDs being stored
|
||||
# with @draft at the end. To reproduce, must update children while in draft mode.
|
||||
Scenario: Problems can be deleted after being public
|
||||
Given I have created a Blank Common Problem
|
||||
And I have created another Blank Common Problem
|
||||
When I publish the unit
|
||||
And I click on "edit a draft"
|
||||
And I delete "1" component
|
||||
And I click on "replace with draft"
|
||||
And I click on "edit a draft"
|
||||
And I delete "1" component
|
||||
Then I see no components
|
||||
|
||||
# This is a very specific scenario for a bug where editing a component in draft
|
||||
# impacted the published version.
|
||||
Scenario: Changes to draft problem do not impact published version
|
||||
Given I have created a Blank Common Problem
|
||||
When I publish the unit
|
||||
And I click on "edit a draft"
|
||||
And I change the display name to "draft"
|
||||
And I click on "delete draft"
|
||||
Then the problem display name is "Blank Common Problem"
|
||||
|
||||
Scenario: Problems can be made private after being made public
|
||||
Given I have created a Blank Common Problem
|
||||
When I publish the unit
|
||||
And I click on "edit a draft"
|
||||
And I click on "delete draft"
|
||||
And I unpublish the unit
|
||||
Then I can edit the problem
|
||||
|
||||
Scenario: Cheat sheet visible on toggle
|
||||
Given I have created a Blank Common Problem
|
||||
And I can edit the problem
|
||||
|
||||
@@ -305,15 +305,13 @@ def i_can_edit_problem(_step):
|
||||
|
||||
@step(u'I edit first blank advanced problem for annotation response$')
|
||||
def i_edit_blank_problem_for_annotation_response(_step):
|
||||
edit_css = """$('.component-header:contains("Blank Advanced Problem")').parent().find('a.edit-button').click()"""
|
||||
world.edit_component(1)
|
||||
text = """
|
||||
<problem>
|
||||
<annotationresponse>
|
||||
<annotationinput><text>Text of annotation</text></annotationinput>
|
||||
</annotationresponse>
|
||||
</problem>"""
|
||||
world.browser.execute_script(edit_css)
|
||||
world.wait_for_ajax_complete()
|
||||
type_in_codemirror(0, text)
|
||||
world.save_component()
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
|
||||
|
||||
# just pick one vertical
|
||||
descriptor = store.get_items(course.id, category='vertical',)
|
||||
resp = self.client.get_html(get_url('unit_handler', descriptor[0].location))
|
||||
resp = self.client.get_html(get_url('container_handler', descriptor[0].location))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
for expected in expected_types:
|
||||
@@ -120,7 +120,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
|
||||
# just pick one vertical
|
||||
usage_key = course_items[0].id.make_usage_key('vertical', None)
|
||||
|
||||
resp = self.client.get_html(get_url('unit_handler', usage_key))
|
||||
resp = self.client.get_html(get_url('container_handler', usage_key))
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def check_edit_unit(self, test_course_name):
|
||||
@@ -926,7 +926,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
|
||||
# Assert is here to make sure that the course being tested actually has verticals (units) to check.
|
||||
self.assertGreater(len(items), 0)
|
||||
for descriptor in items:
|
||||
resp = self.client.get_html(get_url('unit_handler', descriptor.location))
|
||||
resp = self.client.get_html(get_url('container_handler', descriptor.location))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
@@ -1293,7 +1293,7 @@ class ContentStoreTest(ContentStoreTestCase):
|
||||
|
||||
# go look at the Edit page
|
||||
unit_key = course_key.make_usage_key('vertical', 'test_vertical')
|
||||
resp = self.client.get_html(get_url('unit_handler', unit_key))
|
||||
resp = self.client.get_html(get_url('container_handler', unit_key))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def delete_item(category, name):
|
||||
|
||||
@@ -11,7 +11,6 @@ from django.conf import settings
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from edxmako.shortcuts import render_to_response
|
||||
|
||||
from util.date_utils import get_default_time_display
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import PublishState
|
||||
|
||||
@@ -23,7 +22,7 @@ from xblock.plugin import PluginMissingError
|
||||
from xblock.runtime import Mixologist
|
||||
|
||||
from contentstore.utils import get_lms_link_for_item, compute_publish_state
|
||||
from contentstore.views.helpers import get_parent_xblock
|
||||
from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name
|
||||
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
@@ -34,14 +33,13 @@ from django.utils.translation import ugettext as _
|
||||
__all__ = ['OPEN_ENDED_COMPONENT_TYPES',
|
||||
'ADVANCED_COMPONENT_POLICY_KEY',
|
||||
'subsection_handler',
|
||||
'unit_handler',
|
||||
'container_handler',
|
||||
'component_handler'
|
||||
]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# NOTE: unit_handler assumes this list is disjoint from ADVANCED_COMPONENT_TYPES
|
||||
# NOTE: it is assumed that this list is disjoint from ADVANCED_COMPONENT_TYPES
|
||||
COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video']
|
||||
|
||||
# Constants for determining if these components should be enabled for this course
|
||||
@@ -135,84 +133,6 @@ def _load_mixed_class(category):
|
||||
return mixologist.mix(component_class)
|
||||
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def unit_handler(request, usage_key_string):
|
||||
"""
|
||||
The restful handler for unit-specific requests.
|
||||
|
||||
GET
|
||||
html: return html page for editing a unit
|
||||
json: not currently supported
|
||||
"""
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
try:
|
||||
course, item, lms_link = _get_item_in_course(request, usage_key)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
component_templates = get_component_templates(course)
|
||||
|
||||
xblocks = item.get_children()
|
||||
|
||||
# TODO (cpennington): If we share units between courses,
|
||||
# this will need to change to check permissions correctly so as
|
||||
# to pick the correct parent subsection
|
||||
containing_subsection = get_parent_xblock(item)
|
||||
containing_section = get_parent_xblock(containing_subsection)
|
||||
|
||||
# cdodge hack. We're having trouble previewing drafts via jump_to redirect
|
||||
# so let's generate the link url here
|
||||
|
||||
# need to figure out where this item is in the list of children as the
|
||||
# preview will need this
|
||||
index = 1
|
||||
for child in containing_subsection.get_children():
|
||||
if child.location == item.location:
|
||||
break
|
||||
index = index + 1
|
||||
|
||||
preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')
|
||||
|
||||
preview_lms_link = (
|
||||
u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'
|
||||
).format(
|
||||
preview_lms_base=preview_lms_base,
|
||||
lms_base=settings.LMS_BASE,
|
||||
org=course.location.org,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
index=index
|
||||
)
|
||||
|
||||
return render_to_response('unit.html', {
|
||||
'context_course': course,
|
||||
'unit': item,
|
||||
'unit_usage_key': item.location,
|
||||
'child_usage_keys': [block.scope_ids.usage_id for block in xblocks],
|
||||
'component_templates': json.dumps(component_templates),
|
||||
'draft_preview_link': preview_lms_link,
|
||||
'published_preview_link': lms_link,
|
||||
'subsection': containing_subsection,
|
||||
'release_date': (
|
||||
get_default_time_display(containing_subsection.start)
|
||||
if containing_subsection.start is not None else None
|
||||
),
|
||||
'section': containing_section,
|
||||
'new_unit_category': 'vertical',
|
||||
'unit_state': compute_publish_state(item),
|
||||
'published_date': (
|
||||
get_default_time_display(item.published_date)
|
||||
if item.published_date is not None else None
|
||||
),
|
||||
})
|
||||
else:
|
||||
return HttpResponseBadRequest("Only supports html requests")
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@require_GET
|
||||
@login_required
|
||||
@@ -235,25 +155,37 @@ def container_handler(request, usage_key_string):
|
||||
component_templates = get_component_templates(course)
|
||||
ancestor_xblocks = []
|
||||
parent = get_parent_xblock(xblock)
|
||||
while parent and parent.category != 'sequential':
|
||||
|
||||
is_unit_page = is_unit(xblock)
|
||||
unit = xblock if is_unit_page else None
|
||||
|
||||
while parent and parent.category != 'course':
|
||||
if unit is None and is_unit(parent):
|
||||
unit = parent
|
||||
ancestor_xblocks.append(parent)
|
||||
parent = get_parent_xblock(parent)
|
||||
ancestor_xblocks.reverse()
|
||||
|
||||
unit = ancestor_xblocks[0] if ancestor_xblocks else None
|
||||
unit_publish_state = compute_publish_state(unit) if unit else None
|
||||
subsection = get_parent_xblock(unit) if unit else None
|
||||
section = get_parent_xblock(subsection) if subsection else None
|
||||
# TODO: correct with publishing story.
|
||||
unit_publish_state = 'draft'
|
||||
|
||||
return render_to_response('container.html', {
|
||||
'context_course': course, # Needed only for display of menus at top of page.
|
||||
'xblock': xblock,
|
||||
'unit_publish_state': unit_publish_state,
|
||||
'xblock_locator': xblock.location,
|
||||
'unit': None if not ancestor_xblocks else ancestor_xblocks[0],
|
||||
'unit': unit,
|
||||
'is_unit_page': is_unit_page,
|
||||
'subsection': subsection,
|
||||
'section': section,
|
||||
'new_unit_category': 'vertical',
|
||||
'ancestor_xblocks': ancestor_xblocks,
|
||||
'component_templates': json.dumps(component_templates),
|
||||
})
|
||||
else:
|
||||
return HttpResponseBadRequest("Only supports html requests")
|
||||
return HttpResponseBadRequest("Only supports HTML requests")
|
||||
|
||||
|
||||
def get_component_templates(course):
|
||||
@@ -285,16 +217,6 @@ def get_component_templates(course):
|
||||
'video': _("Video")
|
||||
}
|
||||
|
||||
def get_component_display_name(component, default_display_name=None):
|
||||
"""
|
||||
Returns the display name for the specified component.
|
||||
"""
|
||||
component_class = _load_mixed_class(component)
|
||||
if hasattr(component_class, 'display_name') and component_class.display_name.default:
|
||||
return _(component_class.display_name.default)
|
||||
else:
|
||||
return default_display_name
|
||||
|
||||
component_templates = []
|
||||
categories = set()
|
||||
# The component_templates array is in the order of "advanced" (if present), followed
|
||||
@@ -305,7 +227,7 @@ def get_component_templates(course):
|
||||
# add the default template with localized display name
|
||||
# TODO: Once mixins are defined per-application, rather than per-runtime,
|
||||
# this should use a cms mixed-in class. (cpennington)
|
||||
display_name = get_component_display_name(category, _('Blank'))
|
||||
display_name = xblock_type_display_name(category, _('Blank'))
|
||||
templates_for_category.append(create_template_dict(display_name, category))
|
||||
categories.add(category)
|
||||
|
||||
@@ -328,7 +250,7 @@ def get_component_templates(course):
|
||||
for advanced_problem_type in ADVANCED_PROBLEM_TYPES:
|
||||
component = advanced_problem_type['component']
|
||||
boilerplate_name = advanced_problem_type['boilerplate_name']
|
||||
component_display_name = get_component_display_name(component)
|
||||
component_display_name = xblock_type_display_name(component)
|
||||
templates_for_category.append(create_template_dict(component_display_name, component, boilerplate_name))
|
||||
categories.add(component)
|
||||
|
||||
@@ -350,7 +272,7 @@ def get_component_templates(course):
|
||||
if category in ADVANCED_COMPONENT_TYPES and not category in categories:
|
||||
# boilerplates not supported for advanced components
|
||||
try:
|
||||
component_display_name = get_component_display_name(category, default_display_name=category)
|
||||
component_display_name = xblock_type_display_name(category, default_display_name=category)
|
||||
advanced_component_templates['templates'].append(
|
||||
create_template_dict(
|
||||
component_display_name,
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext as _
|
||||
from edxmako.shortcuts import render_to_string, render_to_response
|
||||
from xblock.core import XBlock
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from contentstore.utils import reverse_course_url, reverse_usage_url
|
||||
|
||||
@@ -11,7 +16,7 @@ __all__ = ['edge', 'event', 'landing']
|
||||
EDITING_TEMPLATES = [
|
||||
"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"
|
||||
"add-xblock-component-menu-problem", "xblock-string-field-editor",
|
||||
]
|
||||
|
||||
# points to the temporary course landing page with log in and sign up
|
||||
@@ -70,8 +75,8 @@ def xblock_has_own_studio_page(xblock):
|
||||
are a few exceptions:
|
||||
1. Courses
|
||||
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)
|
||||
- themselves treated as units
|
||||
- a direct child of a unit
|
||||
3. XBlocks with children, except for:
|
||||
- sequentials (aka subsections)
|
||||
- chapters (aka sections)
|
||||
@@ -83,7 +88,7 @@ def xblock_has_own_studio_page(xblock):
|
||||
elif category == 'vertical':
|
||||
parent_xblock = get_parent_xblock(xblock)
|
||||
return is_unit(parent_xblock) if parent_xblock else False
|
||||
elif category in ('sequential', 'chapter'):
|
||||
elif category == 'sequential':
|
||||
return False
|
||||
|
||||
# All other xblocks with children have their own page
|
||||
@@ -97,12 +102,30 @@ def xblock_studio_url(xblock):
|
||||
if not xblock_has_own_studio_page(xblock):
|
||||
return None
|
||||
category = xblock.category
|
||||
parent_xblock = get_parent_xblock(xblock)
|
||||
parent_category = parent_xblock.category if parent_xblock else None
|
||||
if category == 'course':
|
||||
if category in ('course', 'chapter'):
|
||||
return reverse_course_url('course_handler', xblock.location.course_key)
|
||||
elif category == 'vertical' and parent_category == 'sequential':
|
||||
# only show the unit page for verticals directly beneath a subsection
|
||||
return reverse_usage_url('unit_handler', xblock.location)
|
||||
else:
|
||||
return reverse_usage_url('container_handler', xblock.location)
|
||||
|
||||
|
||||
def xblock_type_display_name(xblock, default_display_name=None):
|
||||
"""
|
||||
Returns the display name for the specified type of xblock. Note that an instance can be passed in
|
||||
for context dependent names, e.g. a vertical beneath a sequential is a Unit.
|
||||
|
||||
:param xblock: An xblock instance or the type of xblock.
|
||||
:param default_display_name: The default value to return if no display name can be found.
|
||||
:return:
|
||||
"""
|
||||
|
||||
if hasattr(xblock, 'category'):
|
||||
if is_unit(xblock):
|
||||
return _('Unit')
|
||||
category = xblock.category
|
||||
else:
|
||||
category = xblock
|
||||
component_class = XBlock.load_class(category, select=settings.XBLOCK_SELECT_FUNCTION)
|
||||
if hasattr(component_class, 'display_name') and component_class.display_name.default:
|
||||
return _(component_class.display_name.default)
|
||||
else:
|
||||
return default_display_name
|
||||
|
||||
@@ -348,7 +348,7 @@ def export_handler(request, course_key_string):
|
||||
'raw_err_msg': str(exc),
|
||||
'failed_module': failed_item,
|
||||
'unit': unit,
|
||||
'edit_unit_url': reverse_usage_url("unit_handler", parent.location) if parent else "",
|
||||
'edit_unit_url': reverse_usage_url("container_handler", parent.location) if parent else "",
|
||||
'course_home_url': reverse_course_url("course_handler", course_key),
|
||||
'export_url': export_url
|
||||
})
|
||||
|
||||
@@ -21,18 +21,16 @@ from xblock.fragment import Fragment
|
||||
|
||||
import xmodule
|
||||
from xmodule.tabs import StaticTab, CourseTabList
|
||||
from xmodule.modulestore import PublishState, ModuleStoreEnum
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW
|
||||
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW
|
||||
|
||||
from util.json_request import expect_json, JsonResponse
|
||||
|
||||
from .access import has_course_access
|
||||
from .helpers import xblock_has_own_studio_page
|
||||
from contentstore.utils import compute_publish_state
|
||||
from contentstore.views.helpers import is_unit
|
||||
from contentstore.views.preview import get_preview_fragment
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
@@ -187,7 +185,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
xblock = store.get_item(usage_key)
|
||||
is_read_only = _is_xblock_read_only(xblock)
|
||||
container_views = ['container_preview', 'reorderable_container_child_preview']
|
||||
unit_views = PREVIEW_VIEWS
|
||||
|
||||
# wrap the generated fragment in the xmodule_editor div so that the javascript
|
||||
# can bind to it correctly
|
||||
@@ -204,8 +201,8 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
|
||||
|
||||
store.update_item(xblock, request.user.id)
|
||||
elif view_name in (unit_views + container_views):
|
||||
is_container_view = (view_name in container_views)
|
||||
elif view_name in (PREVIEW_VIEWS + container_views):
|
||||
is_pages_view = view_name == STUDENT_VIEW # Only the "Pages" view uses student view in Studio
|
||||
|
||||
# Determine the items to be shown as reorderable. Note that the view
|
||||
# 'reorderable_container_child_preview' is only rendered for xblocks that
|
||||
@@ -215,27 +212,21 @@ def xblock_view_handler(request, usage_key_string, view_name):
|
||||
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
|
||||
# with the new container view.
|
||||
# Set up the context to be passed to each XBlock's render method.
|
||||
context = {
|
||||
'container_view': is_container_view,
|
||||
'is_pages_view': is_pages_view, # This setting disables the recursive wrapping of xblocks
|
||||
'is_unit_page': is_unit(xblock),
|
||||
'read_only': is_read_only,
|
||||
'root_xblock': xblock if (view_name == 'container_preview') else None,
|
||||
'reorderable_items': reorderable_items
|
||||
}
|
||||
|
||||
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.
|
||||
if not is_container_view:
|
||||
# For non-leaf xblocks, show the special rendering which links to the new container page.
|
||||
if xblock_has_own_studio_page(xblock):
|
||||
template = 'container_xblock_component.html'
|
||||
else:
|
||||
template = 'component.html'
|
||||
fragment.content = render_to_string(template, {
|
||||
|
||||
# Note that the container view recursively adds headers into the preview fragment,
|
||||
# so only the "Pages" view requires that this extra wrapper be included.
|
||||
if is_pages_view:
|
||||
fragment.content = render_to_string('component.html', {
|
||||
'xblock_context': context,
|
||||
'xblock': xblock,
|
||||
'locator': usage_key,
|
||||
@@ -263,10 +254,12 @@ def _is_xblock_read_only(xblock):
|
||||
Returns true if the specified xblock is read-only, meaning that it cannot be edited.
|
||||
"""
|
||||
# We allow direct editing of xblocks in DIRECT_ONLY_CATEGORIES (for example, static pages).
|
||||
if xblock.category in DIRECT_ONLY_CATEGORIES:
|
||||
return False
|
||||
component_publish_state = compute_publish_state(xblock)
|
||||
return component_publish_state == PublishState.public
|
||||
# if xblock.category in DIRECT_ONLY_CATEGORIES:
|
||||
# return False
|
||||
# component_publish_state = compute_publish_state(xblock)
|
||||
# return component_publish_state == PublishState.public
|
||||
# TODO: correct with publishing story.
|
||||
return False
|
||||
|
||||
|
||||
def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout=None,
|
||||
|
||||
@@ -191,8 +191,8 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
"""
|
||||
Wraps the results of rendering an XBlock view in a div which adds a header and Studio action buttons.
|
||||
"""
|
||||
# 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 in PREVIEW_VIEWS:
|
||||
# Only add the Studio wrapper when on the container page. The "Pages" page will remain as is for now.
|
||||
if not context.get('is_pages_view', None) and view in PREVIEW_VIEWS:
|
||||
root_xblock = context.get('root_xblock')
|
||||
is_root = root_xblock and xblock.location == root_xblock.location
|
||||
is_reorderable = _is_xblock_reorderable(xblock, context)
|
||||
|
||||
@@ -3,9 +3,7 @@ Unit tests for the container page.
|
||||
"""
|
||||
|
||||
import re
|
||||
from contentstore.utils import compute_publish_state
|
||||
from contentstore.views.tests.utils import StudioPageTestCase
|
||||
from xmodule.modulestore import PublishState
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
|
||||
@@ -35,10 +33,13 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
'data-locator="{0}" data-course-key="{0.course_key}">'.format(self.child_container.location)
|
||||
),
|
||||
expected_breadcrumbs=(
|
||||
r'<a href="/unit/{}"\s*'
|
||||
r'class="navigation-link navigation-parent">Unit</a>\s*'
|
||||
r'<a href="#" class="navigation-link navigation-current">Split Test</a>'
|
||||
).format(re.escape(unicode(self.vertical.location)))
|
||||
r'<a href="/course/{course}" class="navigation-item navigation-link navigation-parent">\s*Week 1\s*</a>\s*'
|
||||
r'<span class="navigation-item navigation-parent">\s*Lesson 1\s*</span>\s*'
|
||||
r'<a href="/container/{unit}" class="navigation-item navigation-link navigation-parent">\s*Unit\s*</a>'
|
||||
).format(
|
||||
course=re.escape(unicode(self.course.id)),
|
||||
unit=re.escape(unicode(self.vertical.location)),
|
||||
),
|
||||
)
|
||||
|
||||
def test_container_on_container_html(self):
|
||||
@@ -57,15 +58,15 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
'data-locator="{0}" data-course-key="{0.course_key}">'.format(draft_container.location)
|
||||
),
|
||||
expected_breadcrumbs=(
|
||||
r'<a href="/unit/{unit}"\s*'
|
||||
r'class="navigation-link navigation-parent">Unit</a>\s*'
|
||||
r'<a href="/container/{split_test}"\s*'
|
||||
r'class="navigation-link navigation-parent">Split Test</a>\s*'
|
||||
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'
|
||||
r'<a href="/course/{course}" class="navigation-item navigation-link navigation-parent">\s*Week 1\s*</a>\s*'
|
||||
r'<span class="navigation-item navigation-parent">\s*Lesson 1\s*</span>\s*'
|
||||
r'<a href="/container/{unit}" class="navigation-item navigation-link navigation-parent">\s*Unit\s*</a>\s*'
|
||||
r'<a href="/container/{split_test}" class="navigation-item navigation-link navigation-parent">\s*Split Test\s*</a>'
|
||||
).format(
|
||||
course=re.escape(unicode(self.course.id)),
|
||||
unit=re.escape(unicode(self.vertical.location)),
|
||||
split_test=re.escape(unicode(self.child_container.location))
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Test the draft version of the container
|
||||
@@ -82,19 +83,9 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
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.
|
||||
if publish_state == PublishState.public:
|
||||
expected_message = 'you need to edit unit <a href="/unit/{}">Unit</a> as a draft.'
|
||||
else:
|
||||
expected_message = 'your changes will be published with unit <a href="/unit/{}">Unit</a>.'
|
||||
expected_unit_link = expected_message.format(self.vertical.location)
|
||||
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.
|
||||
@@ -102,23 +93,17 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
published_unit = self.store.publish(self.vertical.location, self.user.id)
|
||||
published_child_container = self.store.get_item(self.child_container.location)
|
||||
published_child_vertical = self.store.get_item(self.child_vertical.location)
|
||||
self.validate_preview_html(published_unit, self.container_view,
|
||||
can_edit=False, can_reorder=False, can_add=False)
|
||||
self.validate_preview_html(published_child_container, self.container_view,
|
||||
can_edit=False, can_reorder=False, can_add=False)
|
||||
self.validate_preview_html(published_child_vertical, self.reorderable_child_view,
|
||||
can_edit=False, can_reorder=False, can_add=False)
|
||||
self.validate_preview_html(published_unit, self.container_view)
|
||||
self.validate_preview_html(published_child_container, self.container_view)
|
||||
self.validate_preview_html(published_child_vertical, self.reorderable_child_view)
|
||||
|
||||
def test_draft_container_preview_html(self):
|
||||
"""
|
||||
Verify that a draft xblock's container preview returns the expected HTML.
|
||||
"""
|
||||
self.validate_preview_html(self.vertical, self.container_view,
|
||||
can_edit=True, can_reorder=True, can_add=True)
|
||||
self.validate_preview_html(self.child_container, self.container_view,
|
||||
can_edit=True, can_reorder=True, can_add=True)
|
||||
self.validate_preview_html(self.child_vertical, self.reorderable_child_view,
|
||||
can_edit=True, can_reorder=True, can_add=True)
|
||||
self.validate_preview_html(self.vertical, self.container_view)
|
||||
self.validate_preview_html(self.child_container, self.container_view)
|
||||
self.validate_preview_html(self.child_vertical, self.reorderable_child_view)
|
||||
|
||||
def test_public_child_container_preview_html(self):
|
||||
"""
|
||||
@@ -126,25 +111,11 @@ class ContainerPageTestCase(StudioPageTestCase):
|
||||
"""
|
||||
empty_child_container = self._create_item(self.vertical.location, 'split_test', 'Split Test')
|
||||
published_empty_child_container = self.store.publish(empty_child_container.location, self.user.id)
|
||||
self.validate_preview_html(published_empty_child_container, self.reorderable_child_view,
|
||||
can_reorder=False, can_edit=False, can_add=False)
|
||||
self.validate_preview_html(published_empty_child_container, self.reorderable_child_view, 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 = self._create_item(self.vertical.location, 'split_test', 'Split Test')
|
||||
self.validate_preview_html(empty_child_container, self.reorderable_child_view,
|
||||
can_reorder=True, can_edit=True, can_add=False)
|
||||
|
||||
def _create_item(self, parent_location, category, display_name, **kwargs):
|
||||
"""
|
||||
creates an item in the module store, without publishing it.
|
||||
"""
|
||||
return ItemFactory.create(
|
||||
parent_location=parent_location,
|
||||
category=category,
|
||||
display_name=display_name,
|
||||
publish_item=False,
|
||||
**kwargs
|
||||
)
|
||||
self.validate_preview_html(empty_child_container, self.reorderable_child_view, can_add=False)
|
||||
|
||||
@@ -3,7 +3,7 @@ Unit tests for helpers.py.
|
||||
"""
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.views.helpers import xblock_studio_url
|
||||
from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name
|
||||
from xmodule.modulestore.tests.factories import ItemFactory
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ class HelpersTestCase(CourseTestCase):
|
||||
"""
|
||||
Unit tests for helpers.py.
|
||||
"""
|
||||
|
||||
def test_xblock_studio_url(self):
|
||||
|
||||
# Verify course URL
|
||||
@@ -20,18 +21,19 @@ class HelpersTestCase(CourseTestCase):
|
||||
# Verify chapter URL
|
||||
chapter = ItemFactory.create(parent_location=self.course.location, category='chapter',
|
||||
display_name="Week 1")
|
||||
self.assertIsNone(xblock_studio_url(chapter))
|
||||
self.assertEqual(xblock_studio_url(chapter),
|
||||
u'/course/slashes:MITx+999+Robot_Super_Course')
|
||||
|
||||
# Verify lesson URL
|
||||
sequential = ItemFactory.create(parent_location=chapter.location, category='sequential',
|
||||
display_name="Lesson 1")
|
||||
self.assertIsNone(xblock_studio_url(sequential))
|
||||
|
||||
# Verify vertical URL
|
||||
# Verify unit URL
|
||||
vertical = ItemFactory.create(parent_location=sequential.location, category='vertical',
|
||||
display_name='Unit')
|
||||
self.assertEqual(xblock_studio_url(vertical),
|
||||
u'/unit/i4x://MITx/999/vertical/Unit')
|
||||
u'/container/i4x://MITx/999/vertical/Unit')
|
||||
|
||||
# Verify child vertical URL
|
||||
child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical',
|
||||
@@ -43,3 +45,23 @@ class HelpersTestCase(CourseTestCase):
|
||||
video = ItemFactory.create(parent_location=child_vertical.location, category="video",
|
||||
display_name="My Video")
|
||||
self.assertIsNone(xblock_studio_url(video))
|
||||
|
||||
def test_xblock_type_display_name(self):
|
||||
|
||||
chapter = ItemFactory.create(parent_location=self.course.location, category='chapter')
|
||||
sequential = ItemFactory.create(parent_location=chapter.location, category='sequential')
|
||||
|
||||
# Verify unit type display names
|
||||
vertical = ItemFactory.create(parent_location=sequential.location, category='vertical')
|
||||
self.assertEqual(xblock_type_display_name(vertical), u'Unit')
|
||||
self.assertIsNone(xblock_type_display_name('vertical'))
|
||||
|
||||
# Verify video type display names
|
||||
video = ItemFactory.create(parent_location=vertical.location, category="video")
|
||||
self.assertEqual(xblock_type_display_name(video), u'Video')
|
||||
self.assertEqual(xblock_type_display_name('video'), u'Video')
|
||||
|
||||
# Verify split test type display names
|
||||
split_test = ItemFactory.create(parent_location=vertical.location, category="split_test")
|
||||
self.assertEqual(xblock_type_display_name(split_test), u'Content Experiment')
|
||||
self.assertEqual(xblock_type_display_name('split_test'), u'Content Experiment')
|
||||
|
||||
@@ -293,7 +293,7 @@ class ExportTestCase(CourseTestCase):
|
||||
"""
|
||||
fake_xblock = ItemFactory.create(parent_location=self.course.location, category='aawefawef')
|
||||
self.store.publish(fake_xblock.location, self.user.id)
|
||||
self._verify_export_failure(u'/unit/i4x://MITx/999/course/Robot_Super_Course')
|
||||
self._verify_export_failure(u'/container/i4x://MITx/999/course/Robot_Super_Course')
|
||||
|
||||
def test_export_failure_subsection_level(self):
|
||||
"""
|
||||
@@ -305,7 +305,7 @@ class ExportTestCase(CourseTestCase):
|
||||
category='aawefawef'
|
||||
)
|
||||
|
||||
self._verify_export_failure(u'/unit/i4x://MITx/999/vertical/foo')
|
||||
self._verify_export_failure(u'/container/i4x://MITx/999/vertical/foo')
|
||||
|
||||
def _verify_export_failure(self, expectedText):
|
||||
""" Export failure helper method. """
|
||||
|
||||
@@ -38,7 +38,11 @@ class GetPreviewHtmlTestCase(TestCase):
|
||||
request.session = {}
|
||||
|
||||
# Call get_preview_fragment directly.
|
||||
html = get_preview_fragment(request, html, {}).content
|
||||
context = {
|
||||
'reorderable_items': set(),
|
||||
'read_only': True
|
||||
}
|
||||
html = get_preview_fragment(request, html, context).content
|
||||
|
||||
# Verify student view html is returned, and the usage ID is as expected.
|
||||
self.assertRegexpMatches(
|
||||
|
||||
@@ -21,35 +21,18 @@ class UnitPageTestCase(StudioPageTestCase):
|
||||
category="video", display_name="My Video")
|
||||
self.store = modulestore()
|
||||
|
||||
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.
|
||||
"""
|
||||
html = self.get_page_html(self.vertical)
|
||||
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.
|
||||
"""
|
||||
published_video = self.store.publish(self.video.location, self.user.id)
|
||||
self.validate_preview_html(self.video, STUDENT_VIEW,
|
||||
can_edit=True, can_reorder=True, can_add=False)
|
||||
self.validate_preview_html(self.video, STUDENT_VIEW, can_add=False)
|
||||
|
||||
def test_draft_component_preview_html(self):
|
||||
"""
|
||||
Verify that a draft xblock's preview returns the expected HTML.
|
||||
"""
|
||||
self.validate_preview_html(self.video, STUDENT_VIEW,
|
||||
can_edit=True, can_reorder=True, can_add=False)
|
||||
self.validate_preview_html(self.video, STUDENT_VIEW, can_add=False)
|
||||
|
||||
def test_public_child_container_preview_html(self):
|
||||
"""
|
||||
@@ -61,8 +44,7 @@ class UnitPageTestCase(StudioPageTestCase):
|
||||
ItemFactory.create(parent_location=child_container.location,
|
||||
category='html', display_name='grandchild')
|
||||
published_child_container = self.store.publish(child_container.location, self.user.id)
|
||||
self.validate_preview_html(published_child_container, STUDENT_VIEW,
|
||||
can_reorder=True, can_edit=True, can_add=False)
|
||||
self.validate_preview_html(published_child_container, STUDENT_VIEW, can_add=False)
|
||||
|
||||
def test_draft_child_container_preview_html(self):
|
||||
"""
|
||||
@@ -74,5 +56,4 @@ class UnitPageTestCase(StudioPageTestCase):
|
||||
ItemFactory.create(parent_location=child_container.location,
|
||||
category='html', display_name='grandchild')
|
||||
draft_child_container = self.store.get_item(child_container.location)
|
||||
self.validate_preview_html(draft_child_container, STUDENT_VIEW,
|
||||
can_reorder=True, can_edit=True, can_add=False)
|
||||
self.validate_preview_html(draft_child_container, STUDENT_VIEW, can_add=False)
|
||||
|
||||
@@ -41,19 +41,16 @@ class StudioPageTestCase(CourseTestCase):
|
||||
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):
|
||||
def validate_preview_html(self, xblock, view_name, 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)
|
||||
self.validate_html_for_add_buttons(html, can_add)
|
||||
|
||||
# Verify that there are no drag handles for public blocks
|
||||
# Verify drag handles always appear.
|
||||
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)
|
||||
self.assertIn(drag_handle_html, html)
|
||||
|
||||
# Verify that there are no action buttons for public blocks
|
||||
expected_button_html = [
|
||||
@@ -62,10 +59,7 @@ class StudioPageTestCase(CourseTestCase):
|
||||
'<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)
|
||||
self.assertIn(button_html, html)
|
||||
|
||||
def validate_html_for_add_buttons(self, html, can_add=True):
|
||||
"""
|
||||
|
||||
@@ -225,7 +225,6 @@ define([
|
||||
"js/spec/views/group_configuration_spec",
|
||||
|
||||
"js/spec/views/container_spec",
|
||||
"js/spec/views/unit_spec",
|
||||
"js/spec/views/xblock_spec",
|
||||
"js/spec/views/xblock_editor_spec",
|
||||
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
define ["jquery", "jquery.ui", "gettext", "backbone",
|
||||
"js/views/feedback_notification", "js/views/feedback_prompt",
|
||||
"coffee/src/views/module_edit", "js/models/module_info",
|
||||
"js/views/baseview", "js/views/components/add_xblock"],
|
||||
($, ui, gettext, Backbone, NotificationView, PromptView, ModuleEditView, ModuleModel, BaseView, AddXBlockComponent) ->
|
||||
class UnitEditView extends BaseView
|
||||
events:
|
||||
'click .delete-draft': 'deleteDraft'
|
||||
'click .create-draft': 'createDraft'
|
||||
'click .publish-draft': 'publishDraft'
|
||||
'change .visibility-select': 'setVisibility'
|
||||
"click .component-actions .duplicate-button": 'duplicateComponent'
|
||||
|
||||
initialize: =>
|
||||
@visibilityView = new UnitEditView.Visibility(
|
||||
el: @$('.visibility-select')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@locationView = new UnitEditView.LocationState(
|
||||
el: @$('.section-item.editing a')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@nameView = new UnitEditView.NameEdit(
|
||||
el: @$('.unit-name-input')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@addXBlockComponent = new AddXBlockComponent(
|
||||
collection: @options.templates
|
||||
el: @$('.add-xblock-component')
|
||||
createComponent: (template) =>
|
||||
return @createComponent(template, "Creating new component").done(
|
||||
(editor) ->
|
||||
listPanel = @$newComponentItem.prev()
|
||||
listPanel.append(editor.$el)
|
||||
))
|
||||
@addXBlockComponent.render()
|
||||
|
||||
@model.on('change:state', @render)
|
||||
|
||||
@$newComponentItem = @$('.new-component-item')
|
||||
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: (event, ui) =>
|
||||
analytics.track "Reordered Components",
|
||||
course: course_location_analytics
|
||||
id: unit_location_analytics
|
||||
|
||||
payload = children : @components()
|
||||
saving = new NotificationView.Mini
|
||||
title: gettext('Saving…')
|
||||
saving.show()
|
||||
options = success : =>
|
||||
@model.unset('children')
|
||||
saving.hide()
|
||||
@model.save(payload, options)
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
forcePlaceholderSize: true
|
||||
axis: 'y'
|
||||
items: '> .component'
|
||||
)
|
||||
|
||||
@$('.component').each (idx, element) =>
|
||||
model = new ModuleModel
|
||||
id: $(element).data('locator')
|
||||
new ModuleEditView
|
||||
el: element,
|
||||
onDelete: @deleteComponent,
|
||||
model: model
|
||||
|
||||
createComponent: (data, analytics_message) =>
|
||||
self = this
|
||||
operation = $.Deferred()
|
||||
editor = new ModuleEditView(
|
||||
onDelete: @deleteComponent
|
||||
model: new ModuleModel()
|
||||
)
|
||||
|
||||
callback = ->
|
||||
operation.resolveWith(self, [editor])
|
||||
analytics.track analytics_message,
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
type: editor.$el.data('locator')
|
||||
|
||||
editor.createItem(
|
||||
@$el.data('locator'),
|
||||
data,
|
||||
callback
|
||||
)
|
||||
|
||||
return operation.promise()
|
||||
|
||||
duplicateComponent: (event) =>
|
||||
self = this
|
||||
event.preventDefault()
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
source_locator = $component.data('locator')
|
||||
@runOperationShowingMessage(gettext('Duplicating…'), ->
|
||||
operation = self.createComponent(
|
||||
{duplicate_source_locator: source_locator},
|
||||
"Duplicating " + source_locator);
|
||||
operation.done(
|
||||
(editor) ->
|
||||
originalOffset = @getScrollOffset($component)
|
||||
$component.after(editor.$el)
|
||||
# Scroll the window so that the new component replaces the old one
|
||||
@setScrollOffset(editor.$el, originalOffset)
|
||||
))
|
||||
|
||||
components: => @$('.component').map((idx, el) -> $(el).data('locator')).get()
|
||||
|
||||
wait: (value) =>
|
||||
@$('.unit-body').toggleClass("waiting", value)
|
||||
|
||||
render: =>
|
||||
if @model.hasChanged('state')
|
||||
@$el.toggleClass("edit-state-#{@model.previous('state')} edit-state-#{@model.get('state')}")
|
||||
@wait(false)
|
||||
|
||||
saveDraft: =>
|
||||
@model.save()
|
||||
|
||||
deleteComponent: (event) =>
|
||||
self = this
|
||||
event.preventDefault()
|
||||
@confirmThenRunOperation(gettext('Delete this component?'),
|
||||
gettext('Deleting this component is permanent and cannot be undone.'),
|
||||
gettext('Yes, delete this component'),
|
||||
->
|
||||
self.runOperationShowingMessage(gettext('Deleting…'),
|
||||
->
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
return $.ajax({
|
||||
type: 'DELETE',
|
||||
url: self.model.urlRoot + "/" + $component.data('locator')
|
||||
}).success(=>
|
||||
analytics.track "Deleted a Component",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
id: $component.data('locator')
|
||||
|
||||
$component.remove()
|
||||
# b/c we don't vigilantly keep children up to date
|
||||
# get rid of it before it hurts someone
|
||||
self.model.save({children: self.components()},
|
||||
{
|
||||
success: (model) ->
|
||||
model.unset('children')
|
||||
})
|
||||
)))
|
||||
|
||||
deleteDraft: (event) ->
|
||||
@wait(true)
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: @model.url()
|
||||
}).success(=>
|
||||
|
||||
analytics.track "Deleted Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
window.location.reload()
|
||||
)
|
||||
|
||||
createDraft: (event) ->
|
||||
self = this
|
||||
@disableElementWhileRunning($(event.target), ->
|
||||
self.wait(true)
|
||||
$.postJSON(self.model.url(), {
|
||||
publish: 'create_draft'
|
||||
}, =>
|
||||
analytics.track "Created Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
self.model.set('state', 'draft')
|
||||
)
|
||||
)
|
||||
|
||||
publishDraft: (event) ->
|
||||
self = this
|
||||
@disableElementWhileRunning($(event.target), ->
|
||||
self.wait(true)
|
||||
self.saveDraft()
|
||||
|
||||
$.postJSON(self.model.url(), {
|
||||
publish: 'make_public'
|
||||
}, =>
|
||||
analytics.track "Published Draft",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
|
||||
self.model.set('state', 'public')
|
||||
)
|
||||
)
|
||||
|
||||
setVisibility: (event) ->
|
||||
if @$('.visibility-select').val() == 'private'
|
||||
action = 'make_private'
|
||||
visibility = "private"
|
||||
else
|
||||
action = 'make_public'
|
||||
visibility = "public"
|
||||
|
||||
@wait(true)
|
||||
|
||||
$.postJSON(@model.url(), {
|
||||
publish: action
|
||||
}, =>
|
||||
analytics.track "Set Unit Visibility",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
visibility: visibility
|
||||
|
||||
@model.set('state', @$('.visibility-select').val()))
|
||||
|
||||
class UnitEditView.NameEdit extends BaseView
|
||||
events:
|
||||
'change .unit-display-name-input': 'saveName'
|
||||
|
||||
initialize: =>
|
||||
@model.on('change:metadata', @render)
|
||||
@model.on('change:state', @setEnabled)
|
||||
@setEnabled()
|
||||
@saveName
|
||||
@$spinner = $('<span class="spinner-in-field-icon"></span>');
|
||||
|
||||
render: =>
|
||||
@$('.unit-display-name-input').val(@model.get('metadata').display_name)
|
||||
|
||||
setEnabled: =>
|
||||
disabled = @model.get('state') == 'public'
|
||||
if disabled
|
||||
@$('.unit-display-name-input').attr('disabled', true)
|
||||
else
|
||||
@$('.unit-display-name-input').removeAttr('disabled')
|
||||
|
||||
saveName: =>
|
||||
# Treat the metadata dictionary as immutable
|
||||
metadata = $.extend({}, @model.get('metadata'))
|
||||
metadata.display_name = @$('.unit-display-name-input').val()
|
||||
@model.save(metadata: metadata)
|
||||
# Update name shown in the right-hand side location summary.
|
||||
$('.unit-location .editing .unit-name').html(metadata.display_name)
|
||||
analytics.track "Edited Unit Name",
|
||||
course: course_location_analytics
|
||||
unit_id: unit_location_analytics
|
||||
display_name: metadata.display_name
|
||||
|
||||
|
||||
class UnitEditView.LocationState extends BaseView
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
|
||||
render: =>
|
||||
@$el.toggleClass("#{@model.previous('state')}-item #{@model.get('state')}-item")
|
||||
|
||||
class UnitEditView.Visibility extends BaseView
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
@render()
|
||||
|
||||
render: =>
|
||||
@$el.val(@model.get('state'))
|
||||
|
||||
return UnitEditView
|
||||
|
||||
@@ -241,7 +241,7 @@ function createNewUnit(e) {
|
||||
|
||||
function(data) {
|
||||
// redirect to the edit page
|
||||
window.location = "/unit/" + data['locator'];
|
||||
window.location = "/container/" + data['locator'];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers",
|
||||
define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
|
||||
"js/views/container", "js/models/xblock_info", "jquery.simulate",
|
||||
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function ($, create_sinon, view_helpers, ContainerView, XBlockInfo) {
|
||||
function ($, create_sinon, edit_helpers, ContainerView, XBlockInfo) {
|
||||
|
||||
describe("Container View", function () {
|
||||
|
||||
@@ -34,9 +34,10 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
view_helpers.installViewTemplates();
|
||||
edit_helpers.installMockXBlock();
|
||||
edit_helpers.installViewTemplates();
|
||||
appendSetFixtures('<div class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="' + rootLocator + '"></div>');
|
||||
notificationSpy = view_helpers.createNotificationSpy();
|
||||
notificationSpy = edit_helpers.createNotificationSpy();
|
||||
model = new XBlockInfo({
|
||||
id: rootLocator,
|
||||
display_name: 'Test AB Test',
|
||||
@@ -51,6 +52,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
containerView.remove();
|
||||
});
|
||||
|
||||
@@ -186,11 +188,11 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
|
||||
|
||||
// Drag the first component in Group B to the first group.
|
||||
dragComponentAbove(groupBComponent1, groupAComponent1);
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
respondToRequest(requests, 0, 200);
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
respondToRequest(requests, 1, 200);
|
||||
view_helpers.verifyNotificationHidden(notificationSpy);
|
||||
edit_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it('does not hide saving message if failure', function () {
|
||||
@@ -198,9 +200,9 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
|
||||
|
||||
// Drag the first component in Group B to the first group.
|
||||
dragComponentAbove(groupBComponent1, groupAComponent1);
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
respondToRequest(requests, 0, 500);
|
||||
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
|
||||
|
||||
// Since the first reorder call failed, the removal will not be called.
|
||||
verifyNumReorderCalls(requests, 1);
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
|
||||
"js/views/feedback_prompt", "js/views/pages/container", "js/models/xblock_info"],
|
||||
"js/views/feedback_prompt", "js/views/pages/container", "js/models/xblock_info", "jquery.simulate"],
|
||||
function ($, _, str, create_sinon, edit_helpers, Prompt, ContainerPage, XBlockInfo) {
|
||||
|
||||
describe("ContainerPage", function() {
|
||||
var lastRequest, renderContainerPage, expectComponents, respondWithHtml,
|
||||
model, containerPage, requests,
|
||||
model, containerPage, requests, initialDisplayName,
|
||||
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
|
||||
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'),
|
||||
mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'),
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
|
||||
|
||||
beforeEach(function () {
|
||||
var newDisplayName = 'New Display Name';
|
||||
|
||||
edit_helpers.installEditTemplates();
|
||||
edit_helpers.installTemplate('xblock-string-field-editor');
|
||||
appendSetFixtures(mockContainerPage);
|
||||
|
||||
edit_helpers.installMockXBlock({
|
||||
data: "<p>Some HTML</p>",
|
||||
metadata: {
|
||||
display_name: newDisplayName
|
||||
}
|
||||
});
|
||||
|
||||
initialDisplayName = 'Test Container';
|
||||
|
||||
model = new XBlockInfo({
|
||||
id: 'locator-container',
|
||||
display_name: 'Test Container',
|
||||
display_name: initialDisplayName,
|
||||
category: 'vertical'
|
||||
});
|
||||
containerPage = new ContainerPage({
|
||||
@@ -26,6 +38,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
});
|
||||
|
||||
lastRequest = function() { return requests[requests.length - 1]; };
|
||||
|
||||
respondWithHtml = function(html) {
|
||||
@@ -55,9 +71,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
describe("Initial display", function() {
|
||||
it('can render itself', function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
expect(containerPage.$el.select('.xblock-header')).toBeTruthy();
|
||||
expect(containerPage.$('.wrapper-xblock')).not.toHaveClass('is-hidden');
|
||||
expect(containerPage.$('.no-container-content')).toHaveClass('is-hidden');
|
||||
expect(containerPage.$('.xblock-header').length).toBe(9);
|
||||
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('shows a loading indicator', function() {
|
||||
@@ -70,25 +85,27 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
});
|
||||
|
||||
describe("Editing the container", function() {
|
||||
var newDisplayName = 'New Display Name';
|
||||
var updatedDisplayName = 'Updated Test Container',
|
||||
inlineEditDisplayName, displayNameElement, displayNameInput;
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installMockXBlock({
|
||||
data: "<p>Some HTML</p>",
|
||||
metadata: {
|
||||
display_name: newDisplayName
|
||||
}
|
||||
});
|
||||
beforeEach(function() {
|
||||
displayNameElement = containerPage.$('.page-header-title');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
inlineEditDisplayName = function(newTitle) {
|
||||
displayNameElement.click();
|
||||
expect(displayNameElement).toHaveClass('is-hidden');
|
||||
displayNameInput = containerPage.$('.xblock-string-field-editor .xblock-field-input');
|
||||
expect(displayNameInput).not.toHaveClass('is-hidden');
|
||||
displayNameInput.val(newTitle);
|
||||
};
|
||||
|
||||
it('can edit itself', function() {
|
||||
var editButtons,
|
||||
updatedTitle = 'Updated Test Container';
|
||||
var editButtons;
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
|
||||
// Click the root edit button
|
||||
@@ -118,26 +135,49 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
resources: []
|
||||
});
|
||||
|
||||
// Expect the title and breadcrumb to be updated
|
||||
expect(containerPage.$('.page-header-title').text().trim()).toBe(updatedTitle);
|
||||
expect(containerPage.$('.page-header .subtitle a').last().text().trim()).toBe(updatedTitle);
|
||||
// Expect the title to have been updated
|
||||
expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
|
||||
});
|
||||
|
||||
it('can inline edit the display name', function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
inlineEditDisplayName(updatedDisplayName);
|
||||
displayNameInput.change();
|
||||
create_sinon.respondWithJson(requests, { });
|
||||
expect(displayNameInput).toHaveClass('is-hidden');
|
||||
expect(displayNameElement).not.toHaveClass('is-hidden');
|
||||
expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
|
||||
expect(containerPage.model.get('display_name')).toBe(updatedDisplayName);
|
||||
});
|
||||
|
||||
it('does not change the title when a display name update fails', function() {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
inlineEditDisplayName(updatedDisplayName);
|
||||
displayNameInput.change();
|
||||
create_sinon.respondWithError(requests);
|
||||
expect(displayNameElement).toHaveClass('is-hidden');
|
||||
expect(displayNameInput).not.toHaveClass('is-hidden');
|
||||
expect(displayNameInput.val().trim()).toBe(updatedDisplayName);
|
||||
expect(containerPage.model.get('display_name')).toBe(initialDisplayName);
|
||||
});
|
||||
|
||||
it('can cancel an inline edit', function() {
|
||||
var numRequests;
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
inlineEditDisplayName(updatedDisplayName);
|
||||
numRequests = requests.length;
|
||||
displayNameInput.simulate("keydown", { keyCode: $.simulate.keyCode.ESCAPE });
|
||||
displayNameInput.simulate("keyup", { keyCode: $.simulate.keyCode.ESCAPE });
|
||||
expect(requests.length).toBe(numRequests);
|
||||
expect(displayNameInput).toHaveClass('is-hidden');
|
||||
expect(displayNameElement).not.toHaveClass('is-hidden');
|
||||
expect(displayNameElement.text().trim()).toBe(initialDisplayName);
|
||||
expect(containerPage.model.get('display_name')).toBe(initialDisplayName);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Editing an xblock", function() {
|
||||
var newDisplayName = 'New Display Name';
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installMockXBlock({
|
||||
data: "<p>Some HTML</p>",
|
||||
metadata: {
|
||||
display_name: newDisplayName
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
@@ -190,6 +230,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
});
|
||||
|
||||
modal = $('.edit-xblock-modal');
|
||||
expect(modal.length).toBe(1);
|
||||
// Click on the settings tab
|
||||
modal.find('.settings-button').click();
|
||||
// Change the display name's text
|
||||
@@ -426,7 +467,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
});
|
||||
|
||||
describe('createNewComponent ', function () {
|
||||
var clickNewComponent, verifyComponents;
|
||||
var clickNewComponent;
|
||||
|
||||
clickNewComponent = function (index) {
|
||||
containerPage.$(".new-component .new-component-type a.single-template")[index].click();
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
define(["jquery", "underscore.string", "jasmine", "coffee/src/views/unit", "js/models/module_info",
|
||||
"js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", "jasmine-stealth"],
|
||||
function ($, str, jasmine, UnitEditView, ModuleModel, create_sinon, edit_helpers) {
|
||||
var requests, unitView, initialize, lastRequest, respondWithHtml, verifyComponents, i,
|
||||
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
|
||||
|
||||
respondWithHtml = function(html, requestIndex) {
|
||||
create_sinon.respondWithJson(
|
||||
requests,
|
||||
{ html: html, "resources": [] },
|
||||
requestIndex
|
||||
);
|
||||
};
|
||||
|
||||
initialize = function(test) {
|
||||
var mockXBlockHtml = readFixtures('mock/mock-unit-page-xblock.underscore'),
|
||||
mockChildContainerHtml = readFixtures('mock/mock-unit-page-child-container.underscore'),
|
||||
model;
|
||||
requests = create_sinon.requests(test);
|
||||
model = new ModuleModel({
|
||||
id: 'unit_locator',
|
||||
state: 'draft'
|
||||
});
|
||||
unitView = new UnitEditView({
|
||||
el: $('.main-wrapper'),
|
||||
templates: edit_helpers.mockComponentTemplates,
|
||||
model: model
|
||||
});
|
||||
|
||||
// Respond with renderings for the two xblocks in the unit (the second is itself a child container)
|
||||
respondWithHtml(mockXBlockHtml, 0);
|
||||
respondWithHtml(mockChildContainerHtml, 1);
|
||||
};
|
||||
|
||||
lastRequest = function() { return requests[requests.length - 1]; };
|
||||
|
||||
verifyComponents = function (unit, locators) {
|
||||
var components = unit.$(".component");
|
||||
expect(components.length).toBe(locators.length);
|
||||
for (i = 0; i < locators.length; i++) {
|
||||
expect($(components[i]).data('locator')).toBe(locators[i]);
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
edit_helpers.installMockXBlock();
|
||||
|
||||
// needed to stub out the ajax
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track']);
|
||||
window.course_location_analytics = jasmine.createSpy('course_location_analytics');
|
||||
window.unit_location_analytics = jasmine.createSpy('unit_location_analytics');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
});
|
||||
|
||||
describe("UnitEditView", function() {
|
||||
beforeEach(function() {
|
||||
edit_helpers.installEditTemplates();
|
||||
appendSetFixtures(readFixtures('mock/mock-unit-page.underscore'));
|
||||
});
|
||||
|
||||
describe('duplicateComponent', function() {
|
||||
var clickDuplicate;
|
||||
|
||||
clickDuplicate = function (index) {
|
||||
unitView.$(".duplicate-button")[index].click();
|
||||
};
|
||||
|
||||
it('sends the correct JSON to the server', function () {
|
||||
initialize(this);
|
||||
clickDuplicate(0);
|
||||
edit_helpers.verifyXBlockRequest(requests, {
|
||||
"duplicate_source_locator": "loc_1",
|
||||
"parent_locator": "unit_locator"
|
||||
});
|
||||
});
|
||||
|
||||
it('inserts duplicated component immediately after source upon success', function () {
|
||||
initialize(this);
|
||||
clickDuplicate(0);
|
||||
create_sinon.respondWithJson(requests, {"locator": "duplicated_item"});
|
||||
verifyComponents(unitView, ['loc_1', 'duplicated_item', 'loc_2']);
|
||||
});
|
||||
|
||||
it('inserts duplicated component at end if source at end', function () {
|
||||
initialize(this);
|
||||
clickDuplicate(1);
|
||||
create_sinon.respondWithJson(requests, {"locator": "duplicated_item"});
|
||||
verifyComponents(unitView, ['loc_1', 'loc_2', 'duplicated_item']);
|
||||
});
|
||||
|
||||
it('shows a notification while duplicating', function () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
initialize(this);
|
||||
clickDuplicate(0);
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
edit_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it('does not insert duplicated component upon failure', function () {
|
||||
initialize(this);
|
||||
clickDuplicate(0);
|
||||
create_sinon.respondWithError(requests);
|
||||
verifyComponents(unitView, ['loc_1', 'loc_2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewComponent ', function () {
|
||||
var clickNewComponent;
|
||||
|
||||
clickNewComponent = function () {
|
||||
unitView.$(".new-component .new-component-type a.single-template").click();
|
||||
};
|
||||
|
||||
it('sends the correct JSON to the server', function () {
|
||||
initialize(this);
|
||||
clickNewComponent();
|
||||
edit_helpers.verifyXBlockRequest(requests, {
|
||||
"category": "discussion",
|
||||
"type": "discussion",
|
||||
"parent_locator": "unit_locator"
|
||||
});
|
||||
});
|
||||
|
||||
it('inserts new component at end', function () {
|
||||
initialize(this);
|
||||
clickNewComponent();
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
verifyComponents(unitView, ['loc_1', 'loc_2', 'new_item']);
|
||||
});
|
||||
|
||||
it('shows a notification while creating', function () {
|
||||
var notificationSpy = edit_helpers.createNotificationSpy();
|
||||
initialize(this);
|
||||
clickNewComponent();
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Adding/);
|
||||
create_sinon.respondWithJson(requests, {"locator": "new_item"});
|
||||
edit_helpers.verifyNotificationHidden(notificationSpy);
|
||||
});
|
||||
|
||||
it('does not insert new component upon failure', function () {
|
||||
initialize(this);
|
||||
clickNewComponent();
|
||||
create_sinon.respondWithError(requests);
|
||||
verifyComponents(unitView, ['loc_1', 'loc_2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disabled edit/publish links during ajax call", function() {
|
||||
var link,
|
||||
draft_states = [
|
||||
{
|
||||
state: "draft",
|
||||
selector: ".publish-draft"
|
||||
},
|
||||
{
|
||||
state: "public",
|
||||
selector: ".create-draft"
|
||||
}
|
||||
];
|
||||
|
||||
function test_link_disabled_during_ajax_call(draft_state) {
|
||||
it("re-enables the " + draft_state.selector + " link once the ajax call returns", function() {
|
||||
initialize(this);
|
||||
link = $(draft_state.selector);
|
||||
expect(link).not.toHaveClass('is-disabled');
|
||||
link.click();
|
||||
expect(link).toHaveClass('is-disabled');
|
||||
create_sinon.respondWithError(requests);
|
||||
expect(link).not.toHaveClass('is-disabled');
|
||||
});
|
||||
}
|
||||
|
||||
for (i = 0; i < draft_states.length; i++) {
|
||||
test_link_disabled_during_ajax_call(draft_states[i]);
|
||||
}
|
||||
});
|
||||
|
||||
describe("Editing an xblock", function() {
|
||||
var newDisplayName = 'New Display Name';
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installMockXBlock({
|
||||
data: "<p>Some HTML</p>",
|
||||
metadata: {
|
||||
display_name: newDisplayName
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
edit_helpers.uninstallMockXBlock();
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
it('can show an edit modal for a child xblock', function() {
|
||||
var editButtons;
|
||||
initialize(this);
|
||||
editButtons = unitView.$('.edit-button');
|
||||
// The container renders two mock xblocks
|
||||
expect(editButtons.length).toBe(2);
|
||||
editButtons[1].click();
|
||||
// Make sure that the correct xblock is requested to be edited
|
||||
expect(str.startsWith(lastRequest().url, '/xblock/loc_2/studio_view')).toBeTruthy();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXBlockEditorHtml,
|
||||
resources: []
|
||||
});
|
||||
|
||||
// Expect that a modal is shown with the correct title
|
||||
expect(edit_helpers.isShowingModal()).toBeTruthy();
|
||||
expect(edit_helpers.getModalTitle()).toBe('Editing: Test Child Container');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe("Editing an xmodule", function() {
|
||||
var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'),
|
||||
newDisplayName = 'New Display Name';
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installMockXModule({
|
||||
data: "<p>Some HTML</p>",
|
||||
metadata: {
|
||||
display_name: newDisplayName
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
edit_helpers.uninstallMockXModule();
|
||||
edit_helpers.cancelModalIfShowing();
|
||||
});
|
||||
|
||||
it('can save changes to settings', function() {
|
||||
var editButtons, modal, mockUpdatedXBlockHtml;
|
||||
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
|
||||
initialize(this);
|
||||
editButtons = unitView.$('.edit-button');
|
||||
// The container renders two mock xblocks
|
||||
expect(editButtons.length).toBe(2);
|
||||
editButtons[1].click();
|
||||
create_sinon.respondWithJson(requests, {
|
||||
html: mockXModuleEditor,
|
||||
resources: []
|
||||
});
|
||||
|
||||
modal = $('.edit-xblock-modal');
|
||||
// Click on the settings tab
|
||||
modal.find('.settings-button').click();
|
||||
// Change the display name's text
|
||||
modal.find('.setting-input').text("Mock Update");
|
||||
// Press the save button
|
||||
modal.find('.action-save').click();
|
||||
// Respond to the save
|
||||
create_sinon.respondWithJson(requests, {
|
||||
id: 'mock-id'
|
||||
});
|
||||
|
||||
// Respond to the request to refresh
|
||||
respondWithHtml(mockUpdatedXBlockHtml);
|
||||
|
||||
// Verify that the xblock was updated
|
||||
expect(unitView.$('.mock-updated-content').text()).toBe('Mock Update');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -85,7 +85,7 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper
|
||||
});
|
||||
// Give the mock xblock a save method...
|
||||
editor.xblock.save = window.MockDescriptor.save;
|
||||
editor.save();
|
||||
editor.model.save(editor.getXModuleData());
|
||||
request = requests[requests.length - 1];
|
||||
response = JSON.parse(request.requestBody);
|
||||
expect(response.metadata.display_name).toBe(testDisplayName);
|
||||
|
||||
@@ -83,8 +83,8 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
|
||||
|
||||
// Add templates needed by the settings editor
|
||||
modal_helpers.installTemplate('metadata-editor');
|
||||
modal_helpers.installTemplate('metadata-number-entry');
|
||||
modal_helpers.installTemplate('metadata-string-entry');
|
||||
modal_helpers.installTemplate('metadata-number-entry', false, 'metadata-number-entry');
|
||||
modal_helpers.installTemplate('metadata-string-entry', false, 'metadata-string-entry');
|
||||
};
|
||||
|
||||
showEditModal = function(requests, xblockElement, model, mockHtml, options) {
|
||||
|
||||
@@ -9,9 +9,11 @@ define(['jquery', 'js/views/feedback_notification', 'js/views/feedback_prompt'],
|
||||
verifyNotificationHidden, createPromptSpy, confirmPrompt, verifyPromptShowing,
|
||||
verifyPromptHidden;
|
||||
|
||||
installTemplate = function(templateName, isFirst) {
|
||||
var template = readFixtures(templateName + '.underscore'),
|
||||
installTemplate = function(templateName, isFirst, templateId) {
|
||||
var template = readFixtures(templateName + '.underscore');
|
||||
if (!templateId) {
|
||||
templateId = templateName + '-tpl';
|
||||
}
|
||||
|
||||
if (isFirst) {
|
||||
setFixtures($('<script>', { id: templateId, type: 'text/template' }).text(template));
|
||||
|
||||
@@ -147,10 +147,19 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
|
||||
},
|
||||
|
||||
save: function(event) {
|
||||
var self = this,
|
||||
editorView = this.editorView,
|
||||
xblockInfo = this.xblockInfo,
|
||||
data = editorView.getXModuleData();
|
||||
event.preventDefault();
|
||||
this.editorView.save({
|
||||
success: _.bind(this.onSave, this)
|
||||
});
|
||||
if (data) {
|
||||
this.runOperationShowingMessage(gettext('Saving…'),
|
||||
function() {
|
||||
return xblockInfo.save(data);
|
||||
}).done(function() {
|
||||
self.onSave();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onSave: function() {
|
||||
@@ -177,7 +186,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
|
||||
if (xblockWrapperElement.length > 0) {
|
||||
xblockElement = xblockWrapperElement.find('.xblock');
|
||||
displayName = xblockWrapperElement.find('.xblock-header .header-details .xblock-display-name').text().trim();
|
||||
// If not found, try looking for the old unit page style rendering
|
||||
// If not found, try looking for the old unit page style rendering.
|
||||
// Only used now by static pages.
|
||||
if (!displayName) {
|
||||
displayName = this.xblockElement.find('.component-header').text().trim();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
* This page allows the user to understand and manipulate the xblock and its children.
|
||||
*/
|
||||
define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/container",
|
||||
"js/views/xblock", "js/views/components/add_xblock", "js/views/modals/edit_xblock", "js/models/xblock_info"],
|
||||
function ($, _, gettext, BaseView, ContainerView, XBlockView, AddXBlockComponent, EditXBlockModal, XBlockInfo) {
|
||||
"js/views/xblock", "js/views/components/add_xblock", "js/views/modals/edit_xblock", "js/models/xblock_info",
|
||||
"js/views/xblock_string_field_editor"],
|
||||
function ($, _, gettext, BaseView, ContainerView, XBlockView, AddXBlockComponent, EditXBlockModal, XBlockInfo,
|
||||
XBlockStringFieldEditor) {
|
||||
var XBlockContainerPage = BaseView.extend({
|
||||
// takes XBlockInfo as a model
|
||||
|
||||
@@ -12,6 +14,11 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
|
||||
|
||||
initialize: function() {
|
||||
BaseView.prototype.initialize.call(this);
|
||||
this.nameEditor = new XBlockStringFieldEditor({
|
||||
el: this.$('.wrapper-xblock-field'),
|
||||
model: this.model
|
||||
});
|
||||
this.nameEditor.render();
|
||||
this.xblockView = new ContainerView({
|
||||
el: this.$('.wrapper-xblock'),
|
||||
model: this.model,
|
||||
@@ -36,12 +43,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
|
||||
|
||||
// Render the xblock
|
||||
xblockView.render({
|
||||
success: function(xblock) {
|
||||
success: function() {
|
||||
xblockView.xblock.runtime.notify("page-shown", self);
|
||||
xblockView.$el.removeClass('is-hidden');
|
||||
self.renderAddXBlockComponents();
|
||||
self.onXBlockRefresh(xblockView);
|
||||
self.refreshTitle();
|
||||
self.refreshDisplayName();
|
||||
loadingElement.addClass('is-hidden');
|
||||
self.delegateEvents();
|
||||
}
|
||||
@@ -56,10 +63,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
|
||||
return this.xblockView.model.urlRoot;
|
||||
},
|
||||
|
||||
refreshTitle: function() {
|
||||
var title = this.$('.xblock-header .header-details .xblock-display-name').first().text().trim();
|
||||
this.$('.page-header-title').text(title);
|
||||
this.$('.page-header .subtitle a').last().text(title);
|
||||
refreshDisplayName: function() {
|
||||
var displayName = this.$('.xblock-header .header-details .xblock-display-name').first().text().trim();
|
||||
this.model.set('display_name', displayName);
|
||||
},
|
||||
|
||||
onXBlockRefresh: function(xblockView) {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* XBlockEditorView displays the authoring view of an xblock, and allows the user to switch between
|
||||
* the available modes.
|
||||
*/
|
||||
define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/xblock",
|
||||
"js/views/metadata", "js/collections/metadata", "jquery.inputnumber"],
|
||||
function ($, _, gettext, NotificationView, XBlockView, MetadataView, MetadataCollection) {
|
||||
define(["jquery", "underscore", "gettext", "js/views/xblock", "js/views/metadata", "js/collections/metadata",
|
||||
"jquery.inputnumber"],
|
||||
function ($, _, gettext, XBlockView, MetadataView, MetadataCollection) {
|
||||
|
||||
var XBlockEditorView = XBlockView.extend({
|
||||
// takes XBlockInfo as a model
|
||||
@@ -88,26 +88,6 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
|
||||
return this.metadataEditor;
|
||||
},
|
||||
|
||||
save: function(options) {
|
||||
var xblockInfo = this.model,
|
||||
data,
|
||||
saving;
|
||||
data = this.getXModuleData();
|
||||
if (data) {
|
||||
saving = new NotificationView.Mini({
|
||||
title: gettext('Saving…')
|
||||
});
|
||||
saving.show();
|
||||
return xblockInfo.save(data).done(function() {
|
||||
var success = options.success;
|
||||
saving.hide();
|
||||
if (success) {
|
||||
success();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the data saved for the xmodule. Note that this *does not* work for XBlocks.
|
||||
*/
|
||||
|
||||
101
cms/static/js/views/xblock_string_field_editor.js
Normal file
101
cms/static/js/views/xblock_string_field_editor.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* XBlockStringFieldEditor is a view that allows the user to inline edit an XBlock string field.
|
||||
* Clicking on the field value will hide the text and replace it with an input to allow the user
|
||||
* to change the value. Once the user leaves the field, a request will be sent to update the
|
||||
* XBlock field's value if it has been changed. If the user presses Escape, then any changes will
|
||||
* be removed and the input hidden again.
|
||||
*/
|
||||
define(["jquery", "gettext", "js/views/baseview"],
|
||||
function ($, gettext, BaseView) {
|
||||
|
||||
var XBlockStringFieldEditor = BaseView.extend({
|
||||
events: {
|
||||
'click .xblock-field-value': 'showInput',
|
||||
'change .xblock-field-input': 'updateField',
|
||||
'focusout .xblock-field-input': 'onInputFocusLost',
|
||||
'keyup .xblock-field-input': 'handleKeyUp'
|
||||
},
|
||||
|
||||
// takes XBlockInfo as a model
|
||||
|
||||
initialize: function() {
|
||||
BaseView.prototype.initialize.call(this);
|
||||
this.fieldName = this.$el.data('field');
|
||||
this.template = this.loadTemplate('xblock-string-field-editor');
|
||||
this.model.on('change:' + this.fieldName, this.onChangeField, this);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.append(this.template({
|
||||
value: this.model.get(this.fieldName),
|
||||
fieldName: this.fieldName
|
||||
}));
|
||||
return this;
|
||||
},
|
||||
|
||||
getLabel: function() {
|
||||
return this.$('.xblock-field-value');
|
||||
},
|
||||
|
||||
getInput: function () {
|
||||
return this.$('.xblock-field-input');
|
||||
},
|
||||
|
||||
onInputFocusLost: function() {
|
||||
var currentValue = this.model.get(this.fieldName);
|
||||
if (currentValue === this.getInput().val()) {
|
||||
this.hideInput();
|
||||
}
|
||||
},
|
||||
|
||||
onChangeField: function() {
|
||||
var value = this.model.get(this.fieldName);
|
||||
this.getLabel().text(value);
|
||||
this.getInput().val(value);
|
||||
this.hideInput();
|
||||
},
|
||||
|
||||
showInput: function(event) {
|
||||
var input = this.getInput();
|
||||
event.preventDefault();
|
||||
this.getLabel().addClass('is-hidden');
|
||||
input.removeClass('is-hidden');
|
||||
input.focus();
|
||||
},
|
||||
|
||||
hideInput: function() {
|
||||
this.getLabel().removeClass('is-hidden');
|
||||
this.getInput().addClass('is-hidden');
|
||||
},
|
||||
|
||||
updateField: function() {
|
||||
var xblockInfo = this.model,
|
||||
newValue = this.getInput().val(),
|
||||
requestData = this.createUpdateRequestData(newValue),
|
||||
fieldName = this.fieldName;
|
||||
this.runOperationShowingMessage(gettext('Saving…'),
|
||||
function() {
|
||||
return xblockInfo.save(requestData);
|
||||
}).done(function() {
|
||||
xblockInfo.set(fieldName, newValue);
|
||||
});
|
||||
},
|
||||
|
||||
createUpdateRequestData: function(newValue) {
|
||||
var metadata = {};
|
||||
metadata[this.fieldName] = newValue;
|
||||
return {
|
||||
metadata: metadata
|
||||
};
|
||||
},
|
||||
|
||||
handleKeyUp: function(event) {
|
||||
if (event.keyCode === 27) { // Revert the changes if the user hits escape
|
||||
this.getInput().val(this.model.get(this.fieldName));
|
||||
this.hideInput();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return XBlockStringFieldEditor;
|
||||
}); // end define();
|
||||
@@ -331,11 +331,13 @@ p, ul, ol, dl {
|
||||
bottom: -($baseline*1.5);
|
||||
}
|
||||
|
||||
.navigation-link {
|
||||
// breadcrumb navigation
|
||||
.navigation-item {
|
||||
@extend %cont-truncated;
|
||||
display: inline-block;
|
||||
vertical-align: bottom; // correct for extra padding in FF
|
||||
max-width: 250px;
|
||||
color: $gray;
|
||||
|
||||
&.navigation-current {
|
||||
@extend %ui-disabled;
|
||||
@@ -348,7 +350,12 @@ p, ul, ol, dl {
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-link:before {
|
||||
|
||||
.navigation-link:hover {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.navigation-item:before {
|
||||
content: " / ";
|
||||
margin: ($baseline/4);
|
||||
color: $gray;
|
||||
@@ -358,7 +365,7 @@ p, ul, ol, dl {
|
||||
}
|
||||
}
|
||||
|
||||
.navigation .navigation-link:first-child:before {
|
||||
.navigation .navigation-item:first-child:before {
|
||||
content: "";
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -25,15 +25,6 @@
|
||||
|
||||
// ====================
|
||||
|
||||
.view-unit {
|
||||
|
||||
.unit-location .draggable-drop-indicator {
|
||||
display: none; //needed to not show DnD UI (UI is shared across both views)
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// needed to override ui-window styling for dragging state (outline selectors get too specific)
|
||||
.courseware-section.is-dragging {
|
||||
box-shadow: 0 1px 2px 0 $shadow-d1 !important;
|
||||
@@ -81,4 +72,4 @@ body b {
|
||||
.CodeMirror {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,19 @@
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
|
||||
.page-header {
|
||||
.page-header-title {
|
||||
@extend %t-title;
|
||||
@include font-size(28);
|
||||
@include line-height(32);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle .navigation-link {
|
||||
color: $gray;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
.page-header-title-edit {
|
||||
@extend %t-title4;
|
||||
background: none repeat scroll 0 0 $white;
|
||||
border: 0;
|
||||
box-shadow: 0 0 2px 2px $shadow inset;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +130,158 @@
|
||||
padding: ($baseline*.75) ($baseline*.75) ($baseline) ($baseline*.75);
|
||||
}
|
||||
}
|
||||
|
||||
// location widget
|
||||
.unit-location {
|
||||
border-top: 5px solid $gray-l1;
|
||||
background-color: $white;
|
||||
|
||||
.header {
|
||||
@extend %t-title6;
|
||||
padding: ($baseline/2) ($baseline*.75);
|
||||
background-color: $gray-l4;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-bit {
|
||||
margin: ($baseline*.75);
|
||||
border-bottom: 1px solid $gray-l4;
|
||||
padding-bottom: ($baseline/2);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
@extend %t-title7;
|
||||
margin-bottom: ($baseline/2);
|
||||
font-weight: 600;
|
||||
color: $gray-d1;
|
||||
}
|
||||
|
||||
.tip {
|
||||
@extend %t-copy-sub2;
|
||||
display: inline-block;
|
||||
margin: ($baseline/4) 0;
|
||||
color: $gray-l2;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-unit-id {
|
||||
|
||||
.unit-id-value {
|
||||
@extend %t-copy-sub1;
|
||||
display: inline-block;
|
||||
margin: ($baseline/4) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-unit-tree-location {
|
||||
|
||||
.draggable-drop-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section-name {
|
||||
@extend %t-title8;
|
||||
|
||||
&:hover {
|
||||
background: $blue-l5;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection,
|
||||
.courseware-unit {
|
||||
margin: ($baseline/4) 0 0 ($baseline*.75);
|
||||
}
|
||||
|
||||
.courseware-unit .section-item {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.section-item {
|
||||
@include transition(background $tmg-avg ease-in-out 0);
|
||||
@include box-sizing(border-box);
|
||||
@extend %t-copy-sub2;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
overflow: hidden;
|
||||
padding: 6px 8px 8px 16px;
|
||||
background: $gray-l5;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: $gray;
|
||||
|
||||
&:hover {
|
||||
background: $blue-l5;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
background-color: $orange-l3;
|
||||
}
|
||||
|
||||
.public-item {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.private-item {
|
||||
color: $gray-l1;
|
||||
}
|
||||
|
||||
.draft-item {
|
||||
color: $yellow-d1;
|
||||
}
|
||||
|
||||
.public-item:hover,
|
||||
.private-item:hover,
|
||||
.draft-item:hover {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.draft-item:after,
|
||||
.public-item:after,
|
||||
.private-item:after {
|
||||
@include font-size(9);
|
||||
margin-left: 3px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.draft-item:after {
|
||||
content: "- draft";
|
||||
}
|
||||
|
||||
.private-item:after {
|
||||
content: "- private";
|
||||
}
|
||||
}
|
||||
|
||||
.subsection > .section-item:hover {
|
||||
background-color: $gray-l5;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
@extend %ui-btn-flat-outline;
|
||||
@extend %t-action4;
|
||||
width: 90%;
|
||||
margin: 0 0 ($baseline/2) ($baseline/4);
|
||||
border: 1px solid transparent;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
font-weight: normal;
|
||||
color: $gray-l2;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<%inherit file="base.html" />
|
||||
<%def name="online_help_token()">
|
||||
<%
|
||||
if is_unit_page:
|
||||
return "unit"
|
||||
else:
|
||||
return "container"
|
||||
%>
|
||||
</%def>
|
||||
<%!
|
||||
import json
|
||||
|
||||
from xmodule.modulestore import PublishState
|
||||
from contentstore.views.helpers import xblock_studio_url, EDITING_TEMPLATES
|
||||
from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name, EDITING_TEMPLATES
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
<%block name="title">${_("Container")}</%block>
|
||||
<%block name="title">${xblock.display_name_with_default} ${xblock_type_display_name(xblock)}</%block>
|
||||
<%block name="bodyclass">is-signedin course container view-container</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
@@ -53,28 +61,32 @@ main_xblock_info = {
|
||||
<%block name="content">
|
||||
|
||||
|
||||
<div class="wrapper-mast wrapper" data-location="" data-display-name="" data-category="">
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-actions has-navigation has-subtitle">
|
||||
<h1 class="page-header">
|
||||
<div class="page-header">
|
||||
<small class="navigation navigation-parents subtitle">
|
||||
% for ancestor in ancestor_xblocks:
|
||||
<%
|
||||
ancestor_url = xblock_studio_url(ancestor)
|
||||
%>
|
||||
% if ancestor_url:
|
||||
<a href="${ancestor_url}"
|
||||
class="navigation-link navigation-parent">${ancestor.display_name_with_default | h}</a>
|
||||
<a href="${ancestor_url}" class="navigation-item navigation-link navigation-parent">
|
||||
${ancestor.display_name_with_default | h}
|
||||
</a>
|
||||
% else:
|
||||
<span class="navigation-item navigation-parent">${ancestor.display_name_with_default | h}</span>
|
||||
% endif
|
||||
% endfor
|
||||
<a href="#" class="navigation-link navigation-current">${xblock.display_name_with_default | h}</a>
|
||||
</small>
|
||||
<span class="page-header-title">${xblock.display_name_with_default | h}</span>
|
||||
</h1>
|
||||
<div class="wrapper-xblock-field" data-field="display_name">
|
||||
<h1 class="page-header-title is-editable xblock-field-value">${xblock.display_name_with_default | h}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">${_("Page Actions")}</h3>
|
||||
<ul>
|
||||
% if not unit_publish_state == 'public':
|
||||
% if not is_unit_page and not unit_publish_state == 'public':
|
||||
<li class="action-item action-edit nav-item">
|
||||
<a href="#" class="button edit-button action-button">
|
||||
<i class="icon-pencil"></i>
|
||||
@@ -99,42 +111,44 @@ main_xblock_info = {
|
||||
</div>
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
% if unit:
|
||||
% if unit_publish_state == PublishState.public:
|
||||
<div class="bit-publishing published">
|
||||
<h3 class="title pub-status"><span class="sr">${_("Publishing Status")} </span>${_("Published")}</h3>
|
||||
<p class="copy">
|
||||
<%
|
||||
unit_link=u'<a href="{unit_address}">{unit_display_name}</a>'.format(
|
||||
unit_address=xblock_studio_url(unit),
|
||||
unit_display_name=unit.display_name_with_default,
|
||||
)
|
||||
%>
|
||||
${_('To make changes to the content of this page, you need to edit unit {unit_link} as a draft.'
|
||||
).format(unit_link=unit_link)}
|
||||
</p>
|
||||
% if not is_unit_page:
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What can I do on this page?")}</h3>
|
||||
<ul class="list-details">
|
||||
<li class="item-detail">${_("You can view and edit course components that contain other components on this page. In the case of experiment blocks, this allows you to confirm that you have properly configured your experiment groups and make changes to existing content.")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
% if is_unit_page:
|
||||
<div class="unit-location">
|
||||
<h4 class="header">${_("Unit Location")}</h4>
|
||||
<div class="wrapper-unit-id content-bit">
|
||||
<h5 class="title">Unit Location ID</h5>
|
||||
<p class="unit-id">
|
||||
<span class="unit-id-value" id="unit-location-id-input">${unit.location.name}</span>
|
||||
<span class="tip"><span class="sr">Tip: </span>${_("Use this ID to link to this unit from other places in your course")}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="wrapper-unit-tree-location content-bit">
|
||||
<h5 class="title">Unit Tree Location</h5>
|
||||
<ol>
|
||||
<li class="section">
|
||||
<a href="${xblock_studio_url(section)}" class="section-item section-name">
|
||||
<span class="section-name">${section.display_name_with_default}</span>
|
||||
</a>
|
||||
<ol>
|
||||
<li class="subsection">
|
||||
<div class="section-item">
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
|
||||
</div>
|
||||
${units.enum_units(subsection, actions=False, selected=unit.location)}
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="bit-publishing draft">
|
||||
<h3 class="title pub-status"><span class="sr">${_("Publishing Status")} </span>${_("Draft")}</h3>
|
||||
<p class="copy">
|
||||
<%
|
||||
unit_link=u'<a href="{unit_address}">{unit_display_name}</a>'.format(
|
||||
unit_address=xblock_studio_url(unit),
|
||||
unit_display_name=unit.display_name_with_default,
|
||||
)
|
||||
%>
|
||||
${_('You can edit the content of this page, and your changes will be published with unit {unit_link}.').format(unit_link=unit_link)}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What can I do on this page?")}</h3>
|
||||
<ul class="list-details">
|
||||
<li class="item-detail">${_("You can view and edit course components that contain other components on this page. In the case of experiment blocks, this allows you to confirm that you have properly configured your experiment groups and make changes to existing content.")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from contentstore.views.helpers import xblock_studio_url
|
||||
%>
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<section class="wrapper wrapper-xblock wrapper-component-action-header nopreview" data-locator="${locator}" data-course-key="${xblock.location.course_key}">
|
||||
<div class="component-header">
|
||||
${xblock.display_name_with_default}
|
||||
</div>
|
||||
<ul class="component-actions">
|
||||
<li class="action-item action-edit">
|
||||
<a href="#" class="edit-button action-button">
|
||||
<i class="icon-pencil"></i>
|
||||
<span class="action-button-text">${_("Edit")}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="action-item action-duplicate">
|
||||
<a href="#" data-tooltip="${_("Duplicate")}" class="duplicate-button action-button">
|
||||
<i class="icon-copy"></i>
|
||||
<span class="sr">${_("Duplicate")}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" data-tooltip="${_("Delete")}" class="delete-button action-button">
|
||||
<i class="icon-trash"></i>
|
||||
<span class="sr">${_("Delete")}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div class="xblock-header-secondary">
|
||||
<div class="meta-info">${_('This block contains multiple components.')}</div>
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-view">
|
||||
<a href="${xblock_studio_url(xblock)}" class="action-button">
|
||||
## Translators: this is a verb describing the action of viewing more details
|
||||
<span class="action-button-text">${_('View')}</span>
|
||||
<i class="icon-arrow-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
|
||||
<div class="xblock-message-area">
|
||||
${preview}
|
||||
</div>
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
<div class="wrapper-mast wrapper" data-location="" data-display-name="" data-category="">
|
||||
<header class="mast has-actions has-navigation">
|
||||
<h1 class="page-header">
|
||||
<div class="page-header">
|
||||
<small class="navigation navigation-parents subtitle">
|
||||
<a href="/unit/TestCourse/branch/draft/block/vertical8eb" class="navigation-link navigation-parent">Unit 1</a>
|
||||
<a href="#" class="navigation-link navigation-current">Test Container</a>
|
||||
<a href="/unit/TestCourse/branch/draft/block/vertical8eb" class="navigation-item navigation-link navigation-parent">Unit 1</a>
|
||||
</small>
|
||||
<span class="page-header-title">Test Container</span>
|
||||
</h1>
|
||||
<div class="wrapper-xblock-field is-editable" data-field="display_name">
|
||||
<h1 class="page-header-title xblock-field-value">Test Container</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-actions">
|
||||
<h3 class="sr">Page Actions</h3>
|
||||
@@ -31,9 +32,6 @@
|
||||
<article class="content-primary window">
|
||||
<section class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="locator-container">
|
||||
</section>
|
||||
<div class="no-container-content is-hidden">
|
||||
<p>This page has no content yet.</p>
|
||||
</div>
|
||||
<div class="ui-loading is-hidden">
|
||||
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">Loading...</span></p>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-block-type="vertical" data-locator="locator-container"
|
||||
<div class="xblock" data-locator="locator-container"
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-A">
|
||||
@@ -35,7 +35,7 @@
|
||||
</header>
|
||||
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-block-type="vertical">
|
||||
<div class="xblock">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A1">
|
||||
<section class="wrapper-xblock level-element"
|
||||
@@ -144,7 +144,7 @@
|
||||
</header>
|
||||
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-block-type="vertical">
|
||||
<div class="xblock">
|
||||
<ol class="reorderable-container">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B1">
|
||||
<section class="wrapper-xblock level-element"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
<div class="xblock xblock-student_view xmodule_display xmodule_VerticalModule xblock-initialized" data-runtime-class="PreviewRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_vertical;_131a499ddaa3474194c1aa2eced34455" data-type="None" data-block-type="vertical">
|
||||
<div class="xblock xblock-student_view xmodule_display xmodule_VerticalModule xblock-initialized" data-runtime-class="PreviewRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_vertical;_131a499ddaa3474194c1aa2eced34455" data-type="None">
|
||||
<ol class="reorderable-container">
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<section class="wrapper wrapper-xblock wrapper-component-action-header nopreview" data-locator="locator-child-container">
|
||||
<div class="component-header">
|
||||
Test Child Container
|
||||
</div>
|
||||
<ul class="component-actions">
|
||||
<li class="action-item action-edit">
|
||||
<a href="#" class="edit-button action-button">
|
||||
<i class="icon-pencil"></i>
|
||||
<span class="action-button-text">Edit</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="action-item action-duplicate">
|
||||
<a href="#" data-tooltip="Duplicate" class="duplicate-button action-button">
|
||||
<i class="icon-copy"></i>
|
||||
<span class="sr">Duplicate</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" data-tooltip="Delete" class="delete-button action-button">
|
||||
<i class="icon-trash"></i>
|
||||
<span class="sr">Delete</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div class="xblock-header-secondary">
|
||||
<div class="meta-info">This block contains multiple components.</div>
|
||||
<ul class="actions-list">
|
||||
<li class="action-item action-view">
|
||||
<a href="/container/locator-child-container" class="action-button">
|
||||
<span class="action-button-text">View</span>
|
||||
<i class="icon-arrow-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
|
||||
@@ -1,27 +0,0 @@
|
||||
<div class="wrapper wrapper-component-action-header">
|
||||
<div class="component-header">Mock Component</div>
|
||||
<ul class="component-actions">
|
||||
<li class="action-item action-edit">
|
||||
<a href="#" class="edit-button action-button">
|
||||
<i class="icon-pencil"></i>
|
||||
<span class="action-button-text">Edit</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="action-item action-duplicate">
|
||||
<a href="#" data-tooltip="Duplicate" class="duplicate-button action-button">
|
||||
<i class="icon-copy"></i>
|
||||
<span class="sr">Duplicate this component</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" data-tooltip="Delete" class="delete-button action-button">
|
||||
<i class="icon-trash"></i>
|
||||
<span class="sr">Delete this component</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="xblock xblock-student_view" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock"
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-block-type="mock" tabindex="0">
|
||||
<h2>Mock Component</h2>
|
||||
</div>
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
<div class="xblock" data-block-type="vertical" data-locator="locator-container"
|
||||
<div class="xblock" data-locator="locator-container"
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
|
||||
<ol class="reorderable-container">
|
||||
</ol>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="xblock xblock-studio_view" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock"
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-block-type="mock" tabindex="0">
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" tabindex="0">
|
||||
|
||||
<div class="mock-xblock editor-with-buttons">
|
||||
<h3>Mock XBlock Editor</h3>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="xblock xblock-studio_view" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock"
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-block-type="mock" tabindex="0">
|
||||
data-init="MockXBlock" data-runtime-class="StudioRuntime" tabindex="0">
|
||||
|
||||
<div class="mock-xblock">
|
||||
<h3>Mock XBlock Editor</h3>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="VerticalDescriptor" data-block-type="wrapper" tabindex="0">
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="VerticalDescriptor" tabindex="0">
|
||||
<div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
|
||||
<section class="editor-with-tabs">
|
||||
<div class="edit-header">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="MockDescriptor" data-block-type="wrapper" tabindex="0">
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="MockDescriptor" tabindex="0">
|
||||
<div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
|
||||
</div>
|
||||
<section class="sequence-edit">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="VerticalDescriptor" data-block-type="wrapper" tabindex="0">
|
||||
<div class="xblock xblock-studio_view xmodule_edit xmodule_WrapperDescriptor" data-runtime-class="StudioRuntime" data-init="XBlockToXModuleShim" data-runtime-version="1" data-usage-id="i4x:;_;_AndyA;_ABT101;_wrapper;_wrapper_l1_poll" data-type="VerticalDescriptor" tabindex="0">
|
||||
<section class="sequence-edit">
|
||||
<script id="metadata-editor-tpl" type="text/template">
|
||||
<ul class="list-input settings-list">
|
||||
|
||||
3
cms/templates/js/xblock-string-field-editor.underscore
Normal file
3
cms/templates/js/xblock-string-field-editor.underscore
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="xblock-string-field-editor">
|
||||
<input type="text" value="<%= value %>" class="xblock-field-input page-header-title-edit is-hidden" data-metadata-name="<%= fieldName %>">
|
||||
</div>
|
||||
@@ -1,24 +0,0 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
% if xblock.location != xblock_context['root_xblock'].location:
|
||||
<section class="wrapper-xblock level-nesting is-collapsible" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
|
||||
% endif
|
||||
<header class="xblock-header">
|
||||
<div class="header-details">
|
||||
<a href="#" data-tooltip="${_('Expand or Collapse')}" class="action expand-collapse collapse">
|
||||
<i class="icon-caret-down ui-toggle-expansion"></i>
|
||||
<span class="sr">${_('Expand or Collapse')}</span>
|
||||
</a>
|
||||
<span class="xblock-display-name">${xblock.display_name_with_default | h}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<li class="sr action-item">${_('No Actions')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
${content}
|
||||
</article>
|
||||
% if xblock.location != xblock_context['root_xblock'].location:
|
||||
</section>
|
||||
% endif
|
||||
@@ -8,16 +8,17 @@ xblock_url = xblock_studio_url(xblock)
|
||||
show_inline = xblock.has_children and not xblock_url
|
||||
section_class = "level-nesting" if show_inline else "level-element"
|
||||
collapsible_class = "is-collapsible" if xblock.has_children else ""
|
||||
label = xblock.display_name or xblock.scope_ids.block_type
|
||||
%>
|
||||
|
||||
% if not is_root:
|
||||
% if is_reorderable:
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="${xblock.location}">
|
||||
<li class="studio-xblock-wrapper is-draggable" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
|
||||
% else:
|
||||
<div class="studio-xblock-wrapper" data-locator="${xblock.location}">
|
||||
<div class="studio-xblock-wrapper" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
|
||||
% endif
|
||||
|
||||
<section class="wrapper-xblock ${section_class} ${collapsible_class}" data-course-key="${xblock.location.course_key}">
|
||||
<section class="wrapper-xblock ${section_class} ${collapsible_class}">
|
||||
% endif
|
||||
|
||||
<header class="xblock-header xblock-header-${xblock.category}">
|
||||
@@ -29,7 +30,7 @@ collapsible_class = "is-collapsible" if xblock.has_children else ""
|
||||
<span class="sr">${_('Expand or Collapse')}</span>
|
||||
</a>
|
||||
% endif
|
||||
<span class="xblock-display-name">${xblock.display_name_with_default | h}</span>
|
||||
<span class="xblock-display-name">${label | h}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<%inherit file="base.html" />
|
||||
<%def name="online_help_token()"><% return "unit" %></%def>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
from contentstore.views.helpers import EDITING_TEMPLATES
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%namespace name="units" file="widgets/units.html" />
|
||||
<%block name="title">${_("Individual Unit")}</%block>
|
||||
<%block name="bodyclass">is-signedin course unit view-unit feature-upload</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in EDITING_TEMPLATES:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="js/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script type='text/javascript'>
|
||||
require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit", "js/collections/component_template",
|
||||
"xmodule", "jquery.ui", "coffee/src/main", "xblock/cms.runtime.v1"],
|
||||
function(doc, $, ModuleModel, UnitEditView, ComponentTemplates, xmoduleLoader) {
|
||||
window.unit_location_analytics = '${unit_usage_key}';
|
||||
|
||||
var templates = new ComponentTemplates(${component_templates | n}, {parse: true});
|
||||
|
||||
xmoduleLoader.done(function () {
|
||||
new UnitEditView({
|
||||
el: $('.main-wrapper'),
|
||||
view: 'unit',
|
||||
model: new ModuleModel({
|
||||
id: '${unit_usage_key}',
|
||||
state: '${unit_state}'
|
||||
}),
|
||||
templates: templates
|
||||
});
|
||||
|
||||
$('.new-component-template').each(function(){
|
||||
$emptyEditor = $(this).find('.empty');
|
||||
$(this).prepend($emptyEditor);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="main-wrapper edit-state-${unit_state}" data-locator="${unit_usage_key}" data-course-key="${unit_usage_key.course_key}">
|
||||
<div class="inner-wrapper">
|
||||
<div class="alert editing-draft-alert">
|
||||
<p class="alert-message"><strong>${_("You are editing a draft.")}</strong>
|
||||
% if published_date:
|
||||
${_("This unit was originally published on {date}.").format(date=published_date)}
|
||||
% endif
|
||||
</p>
|
||||
<a href="${published_preview_link}" target="_blank" class="alert-action secondary">${_("View the Live Version")}</a>
|
||||
</div>
|
||||
<div class="main-column">
|
||||
<article class="unit-body window">
|
||||
<p class="unit-name-input"><label for="unit-display-name-input">${_("Display Name:")}</label><input type="text" value="${unit.display_name_with_default | h}" id="unit-display-name-input" class="unit-display-name-input" /></p>
|
||||
<ol class="components">
|
||||
% for usage_key in child_usage_keys:
|
||||
<li class="component" data-locator="${usage_key}" data-course-key="${usage_key.course_key}"/>
|
||||
% endfor
|
||||
</ol>
|
||||
<div class="add-xblock-component new-component-item adding"></div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<%
|
||||
index_url = utils.reverse_course_url('course_handler', context_course.id)
|
||||
subsection_url = utils.reverse_usage_url('subsection_handler', subsection.location)
|
||||
%>
|
||||
<div class="sidebar">
|
||||
<div class="unit-settings window">
|
||||
<h4 class="header">${_("Unit Settings")}</h4>
|
||||
<div class="window-contents">
|
||||
<div class="row visibility">
|
||||
<label for="visibility-select" class="inline-label">${_("Visibility:")}</label>
|
||||
<select name="visibility-select" id="visibility-select" class='visibility-select'>
|
||||
<option value="public">${_("Public")}</option>
|
||||
<option value="private">${_("Private")}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row published-alert">
|
||||
<p class="edit-draft-message">${_('This unit has been published. To make changes, you must {link_start}edit a draft{link_end}.').format(link_start='<a href="#" class="create-draft">', link_end='</a>')}</p>
|
||||
<p class="publish-draft-message">${_('This is a draft of the published unit. To update the live version, you must {link_start}replace it with this draft{link_end}.').format(link_start='<a href="#" class="publish-draft">', link_end='</a>')}</p>
|
||||
</div>
|
||||
<div class="row status">
|
||||
<p>
|
||||
% if release_date is not None:
|
||||
${_("This unit is scheduled to be released to <strong>students</strong> on <strong>{date}</strong> with the subsection {link_start}{name}{link_end}").format(
|
||||
date=release_date,
|
||||
name=subsection.display_name_with_default,
|
||||
link_start=u'<a href="{url}">'.format(url=subsection_url),
|
||||
link_end=u'</a>',
|
||||
)}
|
||||
% else:
|
||||
${_("This unit is scheduled to be released to <strong>students</strong> with the subsection {link_start}{name}{link_end}").format(
|
||||
name=subsection.display_name_with_default,
|
||||
link_start=u'<a href="{url}">'.format(url=subsection_url),
|
||||
link_end=u'</a>',
|
||||
)}
|
||||
% endif
|
||||
</p>
|
||||
</div>
|
||||
<div class="row unit-actions">
|
||||
<a href="#" class="delete-draft delete-button">${_("Delete Draft")}</a>
|
||||
<a href="${draft_preview_link}" target="_blank" class="preview-button">${_("Preview")}</a>
|
||||
<a href="${published_preview_link}" target="_blank" class="view-button">${_("View Live")}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window unit-location">
|
||||
<h4 class="header">${_("Unit Location")}</h4>
|
||||
<div class="window-contents">
|
||||
<div class="row wrapper-unit-id">
|
||||
<p class="unit-id">
|
||||
<label for="unit-location-id-input">${_("Unit Identifier:")}</label>
|
||||
<input type="text" class="url value" id="unit-location-id-input" value="${unit.location.name}" readonly />
|
||||
</p>
|
||||
</div>
|
||||
<div class="unit-tree-location">
|
||||
<ol>
|
||||
<li class="section">
|
||||
<a href="${index_url}" class="section-item section-name">
|
||||
<span class="section-name">${section.display_name_with_default}</span>
|
||||
</a>
|
||||
<ol>
|
||||
<li class="subsection">
|
||||
<a href="${subsection_url}" class="section-item">
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
|
||||
</a>
|
||||
${units.enum_units(subsection, actions=False, selected=unit.location)}
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -108,7 +108,7 @@ from django.core.urlresolvers import reverse
|
||||
</div>
|
||||
</header>
|
||||
<article class="xblock-render">
|
||||
<section class="xblock xblock-student_view xmodule_display xmodule_VideoModule" data-runtime-version="1" data-init="XBlockToXModuleShim" data-handler-prefix="/preview/xblock/i4x:;_;_AndyA;_ABT101;_video;_72b5a0d74e8c4ed4a4d4e6bf67837c09/handler" data-type="Video" data-block-type="video">
|
||||
<section class="xblock xblock-student_view xmodule_display xmodule_VideoModule" data-runtime-version="1" data-init="XBlockToXModuleShim" data-handler-prefix="/preview/xblock/i4x:;_;_AndyA;_ABT101;_video;_72b5a0d74e8c4ed4a4d4e6bf67837c09/handler" data-type="Video">
|
||||
|
||||
|
||||
<h2>Video</h2>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! from contentstore.utils import compute_publish_state, reverse_usage_url %>
|
||||
<%! from contentstore.utils import compute_publish_state %>
|
||||
<%! from contentstore.views.helpers import xblock_studio_url %>
|
||||
|
||||
<!--
|
||||
This def will enumerate through a passed in subsection and list all of the units
|
||||
@@ -24,7 +25,7 @@ This def will enumerate through a passed in subsection and list all of the units
|
||||
selected_class = ''
|
||||
%>
|
||||
<div class="section-item ${selected_class}">
|
||||
<a href="${reverse_usage_url('unit_handler', unit.location)}" class="${unit_state}-item">
|
||||
<a href="${xblock_studio_url(unit)}" class="${unit_state}-item">
|
||||
<span class="unit-name">${unit.display_name_with_default}</span>
|
||||
</a>
|
||||
% if actions:
|
||||
|
||||
@@ -75,7 +75,6 @@ urlpatterns += patterns(
|
||||
url(r'^course/{}?$'.format(settings.COURSE_KEY_PATTERN), 'course_handler', name='course_handler'),
|
||||
url(r'^course_notifications/{}/(?P<action_state_id>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'course_notifications_handler'),
|
||||
url(r'^subsection/{}$'.format(settings.USAGE_KEY_PATTERN), 'subsection_handler'),
|
||||
url(r'^unit/{}$'.format(settings.USAGE_KEY_PATTERN), 'unit_handler'),
|
||||
url(r'^container/{}$'.format(settings.USAGE_KEY_PATTERN), 'container_handler'),
|
||||
url(r'^checklists/{}/(?P<checklist_index>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'checklists_handler'),
|
||||
url(r'^orphan/{}$'.format(settings.COURSE_KEY_PATTERN), 'orphan_handler'),
|
||||
|
||||
@@ -37,11 +37,6 @@ REQUIREJS_WAIT = {
|
||||
"jquery", "js/base", "js/models/course", "js/models/settings/advanced",
|
||||
"js/views/settings/advanced", "codemirror"],
|
||||
|
||||
# Individual Unit (editing)
|
||||
re.compile('^Individual Unit \|'): [
|
||||
"js/base", "coffee/src/views/unit",
|
||||
"coffee/src/views/module_edit"],
|
||||
|
||||
# Content - Outline
|
||||
# Note that calling your org, course number, or display name, 'course' will mess this up
|
||||
re.compile('^Course Outline \|'): [
|
||||
|
||||
@@ -174,8 +174,7 @@ def add_staff_markup(user, has_instructor_access, block, view, frag, context):
|
||||
|
||||
if is_studio_course and is_mongo_course:
|
||||
# build edit link to unit in CMS. Can't use reverse here as lms doesn't load cms's urls.py
|
||||
# reverse for contentstore.views.unit_handler
|
||||
edit_link = "//" + settings.CMS_BASE + '/unit/' + unicode(block.location)
|
||||
edit_link = "//" + settings.CMS_BASE + '/container/' + unicode(block.location)
|
||||
|
||||
# return edit link in rendered HTML for display
|
||||
return wrap_fragment(frag, render_to_string("edit_unit_link.html", {'frag_content': frag.content, 'edit_link': edit_link}))
|
||||
|
||||
@@ -171,7 +171,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
|
||||
Context for rendering the studio "author_view".
|
||||
"""
|
||||
return {
|
||||
'container_view': True,
|
||||
'reorderable_items': set(),
|
||||
'root_xblock': root_xblock,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ class StudioEditableModuleTestCase(BaseVerticalModuleTest):
|
||||
"""
|
||||
reorderable_items = set()
|
||||
context = {
|
||||
'container_view': True,
|
||||
'reorderable_items': reorderable_items,
|
||||
'read_only': False,
|
||||
'root_xblock': self.vertical,
|
||||
|
||||
@@ -57,7 +57,7 @@ class VerticalModuleTestCase(BaseVerticalModuleTest):
|
||||
"""
|
||||
# Vertical shouldn't render children on the unit page
|
||||
context = {
|
||||
'container_view': False,
|
||||
'is_unit_page': True
|
||||
}
|
||||
html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content
|
||||
self.assertNotIn(self.test_html_1, html)
|
||||
@@ -66,7 +66,7 @@ class VerticalModuleTestCase(BaseVerticalModuleTest):
|
||||
# Vertical should render reorderable children on the container page
|
||||
reorderable_items = set()
|
||||
context = {
|
||||
'container_view': True,
|
||||
'is_unit_page': False,
|
||||
'reorderable_items': reorderable_items,
|
||||
}
|
||||
html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content
|
||||
|
||||
@@ -45,9 +45,13 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
|
||||
Renders the Studio preview view, which supports drag and drop.
|
||||
"""
|
||||
fragment = Fragment()
|
||||
root_xblock = context.get('root_xblock')
|
||||
is_root = root_xblock and root_xblock.location == self.location
|
||||
|
||||
# For the container page we want the full drag-and-drop, but for unit pages we want
|
||||
# a more concise version that appears alongside the "View =>" link.
|
||||
if context.get('container_view'):
|
||||
# a more concise version that appears alongside the "View =>" link-- unless it is
|
||||
# the unit page and the vertical being rendered is itself the unit vertical (is_root == True).
|
||||
if is_root or not context.get('is_unit_page'):
|
||||
self.render_children(context, fragment, can_reorder=True, can_add=True)
|
||||
return fragment
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class ContainerPage(PageObject):
|
||||
"""
|
||||
Container page in Studio
|
||||
"""
|
||||
NAME_SELECTOR = 'a.navigation-current'
|
||||
NAME_SELECTOR = '.page-header-title'
|
||||
|
||||
def __init__(self, browser, locator):
|
||||
super(ContainerPage, self).__init__(browser)
|
||||
@@ -126,16 +126,8 @@ class ContainerPage(PageObject):
|
||||
def edit(self):
|
||||
"""
|
||||
Clicks the "edit" button for the first component on the page.
|
||||
|
||||
Same as the implementation in unit.py, unit and component pages will be merging.
|
||||
"""
|
||||
self.q(css='.edit-button').first.click()
|
||||
EmptyPromise(
|
||||
lambda: self.q(css='.xblock-studio_view').present,
|
||||
'Wait for the Studio editor to be present'
|
||||
).fulfill()
|
||||
|
||||
return self
|
||||
return _click_edit(self)
|
||||
|
||||
def add_missing_groups(self):
|
||||
"""
|
||||
@@ -164,7 +156,7 @@ class XBlockWrapper(PageObject):
|
||||
"""
|
||||
url = None
|
||||
BODY_SELECTOR = '.studio-xblock-wrapper'
|
||||
NAME_SELECTOR = '.header-details'
|
||||
NAME_SELECTOR = '.xblock-display-name'
|
||||
|
||||
def __init__(self, browser, locator):
|
||||
super(XBlockWrapper, self).__init__(browser)
|
||||
@@ -210,3 +202,33 @@ class XBlockWrapper(PageObject):
|
||||
@property
|
||||
def preview_selector(self):
|
||||
return self._bounded_selector('.xblock-student_view,.xblock-author_view')
|
||||
|
||||
def go_to_container(self):
|
||||
"""
|
||||
Open the container page linked to by this xblock, and return
|
||||
an initialized :class:`.ContainerPage` for that xblock.
|
||||
"""
|
||||
return ContainerPage(self.browser, self.locator).visit()
|
||||
|
||||
def edit(self):
|
||||
"""
|
||||
Clicks the "edit" button for this xblock.
|
||||
"""
|
||||
return _click_edit(self, self._bounded_selector)
|
||||
|
||||
@property
|
||||
def editor_selector(self):
|
||||
return '.xblock-studio_view'
|
||||
|
||||
|
||||
def _click_edit(page_object, bounded_selector=lambda(x): x):
|
||||
"""
|
||||
Click on the first edit button found and wait for the Studio editor to be present.
|
||||
"""
|
||||
page_object.q(css=bounded_selector('.edit-button')).first.click()
|
||||
EmptyPromise(
|
||||
lambda: page_object.q(css='.xblock-studio_view').present,
|
||||
'Wait for the Studio editor to be present'
|
||||
).fulfill()
|
||||
|
||||
return page_object
|
||||
|
||||
@@ -5,7 +5,7 @@ from bok_choy.page_object import PageObject
|
||||
from bok_choy.promise import EmptyPromise
|
||||
|
||||
from .course_page import CoursePage
|
||||
from .unit import UnitPage
|
||||
from .container import ContainerPage
|
||||
|
||||
|
||||
class CourseOutlineContainer(object):
|
||||
@@ -84,10 +84,10 @@ class CourseOutlineUnit(CourseOutlineChild):
|
||||
|
||||
def go_to(self):
|
||||
"""
|
||||
Open the unit page linked to by this unit link, and return
|
||||
an initialized :class:`.UnitPage` for that unit.
|
||||
Open the container page linked to by this unit link, and return
|
||||
an initialized :class:`.ContainerPage` for that unit.
|
||||
"""
|
||||
return UnitPage(self.browser, self.locator).visit()
|
||||
return ContainerPage(self.browser, self.locator).visit()
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css=self.BODY_SELECTOR).present
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
"""
|
||||
Unit page in Studio
|
||||
"""
|
||||
|
||||
from bok_choy.page_object import PageObject
|
||||
from bok_choy.promise import EmptyPromise, Promise
|
||||
|
||||
from . import BASE_URL
|
||||
from .container import ContainerPage
|
||||
|
||||
|
||||
class UnitPage(PageObject):
|
||||
"""
|
||||
Unit page in Studio
|
||||
"""
|
||||
|
||||
NAME_SELECTOR = '#unit-display-name-input'
|
||||
|
||||
def __init__(self, browser, unit_locator):
|
||||
super(UnitPage, self).__init__(browser)
|
||||
self.unit_locator = unit_locator
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""URL to the pages UI in a course."""
|
||||
return "{}/unit/{}".format(BASE_URL, self.unit_locator)
|
||||
|
||||
def is_browser_on_page(self):
|
||||
|
||||
def _is_finished_loading():
|
||||
# Wait until all components have been loaded
|
||||
number_of_leaf_xblocks = len(self.q(css='{} .xblock-author_view,.xblock-student_view'.format(Component.BODY_SELECTOR)).results)
|
||||
is_done = len(self.q(css=Component.BODY_SELECTOR).results) == number_of_leaf_xblocks
|
||||
return (is_done, is_done)
|
||||
|
||||
# First make sure that an element with the view-unit class is present on the page,
|
||||
# and then wait to make sure that the xblocks are all there
|
||||
return (
|
||||
self.q(css='body.view-unit').present and
|
||||
Promise(_is_finished_loading, 'Finished rendering the xblocks in the unit.').fulfill()
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.q(css=self.NAME_SELECTOR).attrs('value')[0]
|
||||
|
||||
@property
|
||||
def components(self):
|
||||
"""
|
||||
Return a list of components loaded on the unit page.
|
||||
"""
|
||||
return self.q(css=Component.BODY_SELECTOR).map(
|
||||
lambda el: Component(self.browser, el.get_attribute('data-locator'))).results
|
||||
|
||||
def edit_draft(self):
|
||||
"""
|
||||
Started editing a draft of this unit.
|
||||
"""
|
||||
EmptyPromise(
|
||||
lambda: self.q(css='.create-draft').present,
|
||||
'Wait for edit draft link to be present'
|
||||
).fulfill()
|
||||
|
||||
self.q(css='.create-draft').first.click()
|
||||
|
||||
EmptyPromise(
|
||||
lambda: self.q(css='.editing-draft-alert').present,
|
||||
'Wait for draft mode to be activated'
|
||||
).fulfill()
|
||||
|
||||
def set_unit_visibility(self, visibility):
|
||||
"""
|
||||
Set unit visibility state
|
||||
|
||||
Arguments:
|
||||
visibility (str): private or public
|
||||
|
||||
"""
|
||||
self.q(css='select[name="visibility-select"] option[value="{}"]'.format(visibility)).first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
selector = '.edit-button'
|
||||
if visibility == 'private':
|
||||
check_func = lambda: self.q(css=selector).visible
|
||||
elif visibility == 'public':
|
||||
check_func = lambda: not self.q(css=selector).visible
|
||||
|
||||
EmptyPromise(check_func, 'Unit Visibility is {}'.format(visibility)).fulfill()
|
||||
|
||||
|
||||
COMPONENT_BUTTONS = {
|
||||
'advanced_tab': '.editor-tabs li.inner_tab_wrap:nth-child(2) > a',
|
||||
'save_settings': '.action-save',
|
||||
}
|
||||
|
||||
|
||||
class Component(PageObject):
|
||||
"""
|
||||
A PageObject representing an XBlock child on the Studio UnitPage (including
|
||||
the editing controls).
|
||||
"""
|
||||
url = None
|
||||
BODY_SELECTOR = '.component'
|
||||
NAME_SELECTOR = '.component-header'
|
||||
|
||||
def __init__(self, browser, locator):
|
||||
super(Component, self).__init__(browser)
|
||||
self.locator = locator
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
|
||||
|
||||
def _bounded_selector(self, selector):
|
||||
"""
|
||||
Return `selector`, but limited to this particular `CourseOutlineChild` context
|
||||
"""
|
||||
return '{}[data-locator="{}"] {}'.format(
|
||||
self.BODY_SELECTOR,
|
||||
self.locator,
|
||||
selector
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text
|
||||
if titles:
|
||||
return titles[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def preview_selector(self):
|
||||
return self._bounded_selector('.xblock-author_view,.xblock-student_view')
|
||||
|
||||
def edit(self):
|
||||
"""
|
||||
Clicks the "edit" button for the first component on the page.
|
||||
|
||||
Same as the implementation in unit.py, unit and component pages will be merging.
|
||||
"""
|
||||
self.q(css=self._bounded_selector('.edit-button')).first.click()
|
||||
EmptyPromise(
|
||||
lambda: self.q(css='.xblock-studio_view').present,
|
||||
'Wait for the Studio editor to be present'
|
||||
).fulfill()
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def editor_selector(self):
|
||||
return '.xblock-studio_view'
|
||||
|
||||
def go_to_container(self):
|
||||
"""
|
||||
Open the container page linked to by this component, and return
|
||||
an initialized :class:`.ContainerPage` for that xblock.
|
||||
"""
|
||||
return ContainerPage(self.browser, self.locator).visit()
|
||||
|
||||
def _click_button(self, button_name):
|
||||
"""
|
||||
Click on a button as specified by `button_name`
|
||||
|
||||
Arguments:
|
||||
button_name (str): button name
|
||||
|
||||
"""
|
||||
self.q(css=COMPONENT_BUTTONS[button_name]).first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def open_advanced_tab(self):
|
||||
"""
|
||||
Click on Advanced Tab.
|
||||
"""
|
||||
self._click_button('advanced_tab')
|
||||
|
||||
def save_settings(self):
|
||||
"""
|
||||
Click on settings Save button.
|
||||
"""
|
||||
self._click_button('save_settings')
|
||||
|
||||
def go_to_group_configuration_page(self):
|
||||
"""
|
||||
Go to the Group Configuration used by the component.
|
||||
"""
|
||||
self.q(css=self._bounded_selector('span.message-text a')).first.click()
|
||||
|
||||
@property
|
||||
def group_configuration_link_name(self):
|
||||
"""
|
||||
Get Group Configuration name from link.
|
||||
"""
|
||||
return self.q(css=self._bounded_selector('span.message-text a')).first.text[0]
|
||||
@@ -73,7 +73,7 @@ class XBlockAcidBase(WebAppTest):
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
unit = subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
|
||||
acid_block = AcidView(self.browser, unit.components[0].preview_selector)
|
||||
acid_block = AcidView(self.browser, unit.xblocks[0].preview_selector)
|
||||
self.validate_acid_block_preview(acid_block)
|
||||
|
||||
def test_acid_block_editor(self):
|
||||
@@ -85,9 +85,7 @@ class XBlockAcidBase(WebAppTest):
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
unit = subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
|
||||
unit.edit_draft()
|
||||
|
||||
acid_block = AcidView(self.browser, unit.components[0].edit().editor_selector)
|
||||
acid_block = AcidView(self.browser, unit.xblocks[0].edit().editor_selector)
|
||||
self.assertTrue(acid_block.init_fn_passed)
|
||||
self.assertTrue(acid_block.resource_url_passed)
|
||||
self.assertTrue(acid_block.scope_passed('content'))
|
||||
@@ -141,15 +139,11 @@ class XBlockAcidParentBase(XBlockAcidBase):
|
||||
self.outline.visit()
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
unit = subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
container = unit.components[0].go_to_container()
|
||||
container = unit.xblocks[0].go_to_container()
|
||||
|
||||
acid_block = AcidView(self.browser, container.xblocks[0].preview_selector)
|
||||
self.validate_acid_block_preview(acid_block)
|
||||
|
||||
@skip('This will fail until the container page supports editing')
|
||||
def test_acid_block_editor(self):
|
||||
super(XBlockAcidParentBase, self).test_acid_block_editor()
|
||||
|
||||
|
||||
class XBlockAcidEmptyParentTest(XBlockAcidParentBase):
|
||||
"""
|
||||
@@ -212,7 +206,6 @@ class XBlockAcidChildTest(XBlockAcidParentBase):
|
||||
|
||||
self.user = course_fix.user
|
||||
|
||||
|
||||
@skip('This will fail until we fix support of children in pure XBlocks')
|
||||
def test_acid_block_preview(self):
|
||||
super(XBlockAcidChildTest, self).test_acid_block_preview()
|
||||
|
||||
@@ -34,17 +34,16 @@ class ContainerBase(StudioCourseTest):
|
||||
self.course_info['run']
|
||||
)
|
||||
|
||||
def go_to_container_page(self, make_draft=False):
|
||||
def go_to_nested_container_page(self):
|
||||
"""
|
||||
Go to the test container page.
|
||||
|
||||
If make_draft is true, the unit page (accessed on way to container page) will be put into draft mode.
|
||||
Go to the nested container page.
|
||||
"""
|
||||
unit = self.go_to_unit_page(make_draft)
|
||||
container = unit.components[0].go_to_container()
|
||||
unit = self.go_to_unit_page()
|
||||
# The 0th entry is the unit page itself.
|
||||
container = unit.xblocks[1].go_to_container()
|
||||
return container
|
||||
|
||||
def go_to_unit_page(self, make_draft=False):
|
||||
def go_to_unit_page(self):
|
||||
"""
|
||||
Go to the test unit page.
|
||||
|
||||
@@ -52,10 +51,7 @@ class ContainerBase(StudioCourseTest):
|
||||
"""
|
||||
self.outline.visit()
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
unit = subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
if make_draft:
|
||||
unit.edit_draft()
|
||||
return unit
|
||||
return subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
|
||||
def verify_ordering(self, container, expected_orderings):
|
||||
"""
|
||||
@@ -83,13 +79,13 @@ class ContainerBase(StudioCourseTest):
|
||||
"""
|
||||
Perform the supplied action and then verify the resulting ordering.
|
||||
"""
|
||||
container = self.go_to_container_page(make_draft=True)
|
||||
container = self.go_to_nested_container_page()
|
||||
action(container)
|
||||
|
||||
self.verify_ordering(container, expected_ordering)
|
||||
|
||||
# Reload the page to see that the change was persisted.
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
self.verify_ordering(container, expected_ordering)
|
||||
|
||||
|
||||
@@ -101,9 +97,9 @@ class NestedVerticalTest(ContainerBase):
|
||||
Sets up a course structure with nested verticals.
|
||||
"""
|
||||
self.container_title = ""
|
||||
self.group_a = "Expand or Collapse\nGroup A"
|
||||
self.group_b = "Expand or Collapse\nGroup B"
|
||||
self.group_empty = "Expand or Collapse\nGroup Empty"
|
||||
self.group_a = "Group A"
|
||||
self.group_b = "Group B"
|
||||
self.group_empty = "Group Empty"
|
||||
self.group_a_item_1 = "Group A Item 1"
|
||||
self.group_a_item_2 = "Group A Item 2"
|
||||
self.group_b_item_1 = "Group B Item 1"
|
||||
@@ -360,13 +356,13 @@ class EditContainerTest(NestedVerticalTest):
|
||||
"""
|
||||
Test the "edit" button on a container appearing on the unit page.
|
||||
"""
|
||||
unit = self.go_to_unit_page(make_draft=True)
|
||||
component = unit.components[0]
|
||||
unit = self.go_to_unit_page()
|
||||
component = unit.xblocks[1]
|
||||
self.modify_display_name_and_verify(component)
|
||||
|
||||
def test_edit_container_on_container_page(self):
|
||||
"""
|
||||
Test the "edit" button on a container appearing on the container page.
|
||||
"""
|
||||
container = self.go_to_container_page(make_draft=True)
|
||||
container = self.go_to_nested_container_page()
|
||||
self.modify_display_name_and_verify(container)
|
||||
|
||||
@@ -57,7 +57,7 @@ class SplitTestMixin(object):
|
||||
|
||||
def verify_add_missing_groups_button_not_present(self, container):
|
||||
"""
|
||||
Checks that the "add missing gorups" button/link is not present.
|
||||
Checks that the "add missing groups" button/link is not present.
|
||||
"""
|
||||
def missing_groups_button_not_present():
|
||||
button_present = container.missing_groups_button_present()
|
||||
@@ -105,9 +105,9 @@ class SplitTest(ContainerBase, SplitTestMixin):
|
||||
|
||||
Returns the container page.
|
||||
"""
|
||||
unit = self.go_to_unit_page(make_draft=True)
|
||||
unit = self.go_to_unit_page()
|
||||
add_advanced_component(unit, 0, 'split_test')
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
container.edit()
|
||||
component_editor = ComponentEditorView(self.browser, container.locator)
|
||||
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
|
||||
@@ -119,16 +119,16 @@ class SplitTest(ContainerBase, SplitTestMixin):
|
||||
],
|
||||
},
|
||||
})
|
||||
return self.go_to_container_page()
|
||||
return self.go_to_nested_container_page()
|
||||
|
||||
def test_create_and_select_group_configuration(self):
|
||||
"""
|
||||
Tests creating a split test instance on the unit page, and then
|
||||
assigning the group configuration.
|
||||
"""
|
||||
unit = self.go_to_unit_page(make_draft=True)
|
||||
unit = self.go_to_unit_page()
|
||||
add_advanced_component(unit, 0, 'split_test')
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
container.edit()
|
||||
component_editor = ComponentEditorView(self.browser, container.locator)
|
||||
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
|
||||
@@ -136,14 +136,14 @@ class SplitTest(ContainerBase, SplitTestMixin):
|
||||
|
||||
# Switch to the other group configuration. Must navigate again to the container page so
|
||||
# that there is only a single "editor" on the page.
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
container.edit()
|
||||
component_editor = ComponentEditorView(self.browser, container.locator)
|
||||
component_editor.set_select_value_and_save('Group Configuration', 'Configuration 0,1,2')
|
||||
self.verify_groups(container, ['Group 0', 'Group 1', 'Group 2'], ['alpha', 'beta'])
|
||||
|
||||
# Reload the page to make sure the groups were persisted.
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
self.verify_groups(container, ['Group 0', 'Group 1', 'Group 2'], ['alpha', 'beta'])
|
||||
|
||||
@skip("This fails periodically where it fails to trigger the add missing groups action.Dis")
|
||||
@@ -161,7 +161,7 @@ class SplitTest(ContainerBase, SplitTestMixin):
|
||||
self.verify_groups(container, ['alpha', 'gamma'], ['beta'])
|
||||
|
||||
# Reload the page to make sure the groups were persisted.
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
self.verify_groups(container, ['alpha', 'gamma'], ['beta'])
|
||||
|
||||
@skip("Disabling as this fails intermittently. STUD-2003")
|
||||
|
||||
Reference in New Issue
Block a user